0

Hi everyone and sorry for the title, I'm not really sure of how to describe my problem. If you have a better title feel free to edit !

A little bit of context

I'm working on a little personal project to help me learn headless & micro-services. So I have an API made with Node.js & Express that works pretty well. I then have my front project which is a simple one-page vue app that use vuex store.

On my single page I have several components and I want to add on each of them a possibility that when you're logged in as an Administrator you can click on every component to edit them.

I made it works well on static elements :

enter image description here

For example, here the plus button is shown as expected.

However, just bellow this one I have some components, that are loaded once the data are received. And in those components, I also have those buttons, but they're not shown. However, there's no data in this one except the title but that part is working very well, already tested and in production. It's just the "admin buttons" part that is not working as I expect it to be :

enter image description here

Sometimes when I edit some codes and the webpack watcher deal with my changes I have the result that appears :

enter image description here

And that's what I expect once the data are loaded.

There is something that I don't understand here and so I can't deal with the problem. Maybe a watch is missing or something ?

So and the code ?

First of all, we have a mixin for "Auth" that isn't implemented yet so for now it's just this :

Auth.js

    export default {
        computed: {
            IsAdmin() {
                return true;
            }
        },
    }

Then we have a first component :

LCSkills.js

<template>
    <div class="skills-container">
        <h2 v-if="skills">{{ $t('skills') }}</h2>
        <LCAdmin v-if="IsAdmin" :addModal="$refs.addModal" />
        <LCModal ref="addModal"></LCModal>
        <div class="skills" v-if="skills">
            <LCSkillCategory
                v-for="category in skills"
                :key="category"
                :category="category"
            />
        </div>
    </div>
</template>

<script>
import LCSkillCategory from './LCSkillCategory.vue';
import { mapState } from 'vuex';
import LCAdmin from '../LCAdmin.vue';
import LCModal from '../LCModal.vue';
import Auth from '../../mixins/Auth';

export default {
    name: 'LCSkills',
    components: {
        LCSkillCategory,
        LCAdmin,
        LCModal,
    },
    computed: mapState({
        skills: (state) => state.career.skills,
    }),
    mixins: [Auth],
};
</script>

<style scoped>
...
</style>

This component load each skills category with the LCSkillCategory component when the data is present in the store.

LCSkillCategory.js

<template>
    <div class="skillsCategory">
        <h2 v-if="category">{{ name }}</h2>
        <LCAdmin
            v-if="IsAdmin && category"
            :editModal="$refs.editModal"
            :deleteModal="$refs.deleteModal"
        />
        <LCModal ref="editModal"></LCModal>
        <LCModal ref="deleteModal"></LCModal>
        <div v-if="category">
            <LCSkill
                v-for="skill in category.skills"
                :key="skill"
                :skill="skill"
            />
        </div>
        <LCAdmin v-if="IsAdmin" :addModal="$refs.addSkillModal" />
        <LCModal ref="addSkillModal"></LCModal>
    </div>
</template>

<script>
import LCSkill from './LCSkill.vue';
import { mapState } from 'vuex';
import LCAdmin from '../LCAdmin.vue';
import LCModal from '../LCModal.vue';
import Auth from '../../mixins/Auth';

export default {
    name: 'LCSkillCategory',
    components: { LCSkill, LCAdmin, LCModal },
    props: ['category'],
    mixins: [Auth],
    computed: mapState({
        name: function() {
            return this.$store.getters['locale/getLocalizedValue']({
                src: this.category,
                attribute: 'name',
            });
        },
    }),
};
</script>

<style scoped>
...
</style>

And so each category load a LCSkill component for each skill of this category.

<template>
    <div class="skill-item">
        <img :src="img(skill.icon.hash, 30, 30)" />
        <p>{{ name }}</p>
        <LCAdmin
            v-if="IsAdmin"
            :editModal="$refs.editModal"
            :deleteModal="$refs.deleteModal"
        />
        <LCModal ref="editModal"></LCModal>
        <LCModal ref="deleteModal"></LCModal>
    </div>
</template>

<script>
import LCImageRendering from '../../mixins/LCImageRendering';
import { mapState } from 'vuex';
import Auth from '../../mixins/Auth';
import LCAdmin from '../LCAdmin.vue';
import LCModal from '../LCModal.vue';

export default {
    name: 'LCSkill',
    mixins: [LCImageRendering, Auth],
    props: ['skill'],
    components: { LCAdmin, LCModal },
    computed: mapState({
        name: function() {
            return this.$store.getters['locale/getLocalizedValue']({
                src: this.skill,
                attribute: 'name',
            });
        },
    }),
};
</script>

<style scoped>
...
</style>

Then, the component with the button that is added everywhere :

LCAdmin.js

<template>
    <div class="lc-admin">
        <button v-if="addModal" @click="addModal.openModal()">
            <i class="fas fa-plus"></i>
        </button>
        <button v-if="editModal" @click="editModal.openModal()">
            <i class="fas fa-edit"></i>
        </button>
        <button v-if="deleteModal" @click="deleteModal.openModal()">
            <i class="fas fa-trash"></i>
        </button>
    </div>
</template>

<script>
export default {
    name: 'LCAdmin',
    props: ['addModal', 'editModal', 'deleteModal'],
};
</script>

Again and I'm sorry it's not that I haven't look for a solution by myself, it's just that I don't know what to lookup for... And I'm also sorry for the very long post...

By the way, if you have some advice about how it is done and how I can improve it, feel free, Really. That how I can learn to do better !

EDIT :: ADDED The Store Code

Store Career Module

import { getCareer, getSkills } from '../../services/CareerService';

const state = () => {
    // eslint-disable-next-line no-unused-labels
    careerPath: [];
    // eslint-disable-next-line no-unused-labels
    skills: [];
};

const actions = {
    async getCareerPath ({commit}) {
        getCareer().then(response => {
            commit('setCareerPath', response);
        }).catch(err => console.log(err));
    },
    async getSkills ({commit}) {
        getSkills().then(response => {
            commit('setSkills', response);
        }).catch(err => console.log(err));
    }
};

const mutations = {
    async setCareerPath(state, careerPath) {
        state.careerPath = careerPath;
    },
    async setSkills(state, skills) {
        state.skills = skills;
    }
}

export default {
    namespaced: true,
    state,
    actions,
    mutations
}

Career Service

export async function getCareer() {
    const response = await fetch('/api/career');
    return await response.json();
}

export async function getSkills() {
    const response = await fetch('/api/career/skill');
    return await response.json();
}

Then App.vue, created() :

created() {
        this.$store.dispatch('config/getConfigurations');
        this.$store.dispatch('certs/getCerts');
        this.$store.dispatch('career/getSkills');
        this.$store.dispatch('projects/getProjects');
    },

Clues

It seems that if I remove the v-if on the buttons of the LCAdmin, the button are shown as expected except that they all show even when I don't want them to. (If no modal are associated)

Which give me this result :

enter image description here

3
  • Sounds bit like reactivity issue. Show the code for the store pls... Commented Jul 8, 2021 at 13:24
  • Edited with the store module file, the service for the call and the created of the App.vue that use the store to load data Commented Jul 8, 2021 at 13:35
  • Btw If I remove the v-if on the button of the LCAdmin everything seems to display properly. That could help. I just tried that. Commented Jul 8, 2021 at 13:38

1 Answer 1

1

Problem is that refs are not reactive

$refs are only populated after the component has been rendered, and they are not reactive. It is only meant as an escape hatch for direct child manipulation - you should avoid accessing $refs from within templates or computed properties.

See simple demo below...

const vm = new Vue({
  el: "#app",
  components: {
    MyComponent: {
      props: ['modalRef'],
      template: `
        <div>
          Hi!
          <button v-if="modalRef">Click!</button>
        </div>`
    }
  },
  data() {
    return {
    }
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <my-component :modal-ref="$refs.modal"></my-component>
  <div ref="modal">I'm modal placeholder</div>
</div>

The solution is to not pass $ref as prop at all. Pass simple true/false (which button to display). And on click event, $emit the event to the parent and pass the name of the ref as string...

Sign up to request clarification or add additional context in comments.

4 Comments

So I can consider that using refs the way I did to pass the modal component to my Admin Component is a bad practice. What would be the best practice ?
So far I see : Adding Modals in the Admin Component Directly or using slots in the Admin component for modals ? Isn't it a little weird ?
It is not only bad practice, it simply does not work! The solution is to not pass $ref as prop at all. Pass simple true/false (which button to display). And on click event, $emit the event to the parent and pass the name of the ref as string...
I see ! Thanks for your time, I really appreciate :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.