Enforce that only one child component can trigger once?


#1

Hi there,

I would really appreciate a pointer on how to implement the following component.

<answer-once>
  <button @click="doA">A</button>
  <button @click="doB">B</button>
  <button @click="doC">C</button>
</answer-once>

The idea is that the answer-once component would enforce that only one of its child buttons can be clicked. Once one of the child buttons gets clicked, the other buttons should get tagged with a CSS disabled class.

And if it’s not tricky enough, I need to enforce this behavior at the answer-once component level because I want the user facing syntax for declaring the buttons to be in template land (i.e. as close to the above code snippet as possible).

Can this be done in Vue?

Thanks for your help!
Cassandra


#2

You might be able to accomplish this with scoped slots.


#3

Thanks for the response!

I didn’t get it fully working as in the problem statement, where I wanted to be able to pass in an arbitrary function to each child via the template…

But this gets it 80% there, if I am willing to restrict myself to a single parameterized function at the root level (aka this.$root.show(ref_name)).

So my follow-up question for the remaining 20% is… can I pass in a function via a property? :slight_smile:

Cassandra

 <once-group>
    <once-button value="choiceA">press me for A</once-button>
    <once-button value="choiceB">press me for B</once-button>
 </once-group>

Vue.component('once-group', {
  data: function(){
    return {
      isClickable: true
    }
  },
  template: `<div><slot></slot></div>`,
  methods: {
    childClicked: function(ref_name) {
      if (this.isClickable){
        this.$root.show(ref_name);        
      }
      this.isClickable = false;
    }
  }
});

Vue.component('once-button', {
  props: {
    value: {type: String, required: true}, // ref_name
  },
  template: '<button @click="$parent.childClicked(value)"><slot></slot></button>'
});

#4

Yes. But if you want to go this route where you have tightly coupled parent/child you can actually use provide/inject to give the child component a method or properties. Here is just one way you might use it (there are multiple ways you could accomplish this).

export default {
  name: 'once-group',
  provide () {
    return {
      click: this.click,
    };
  },
  data () {
    return {
      clicked: false,
    };
  },
  methods: {
    click (fn) {
      if (!this.clicked) {
        fn();
        this.clicked = true;
      }
    },
  },
};
export default {
  name: 'once-button',
  inject: ['click'],
  methods: {
    doSomething() {
      this.click(() => {
        // do something here
      })
    }
  },
};

#5

Thanks for all the pointers! And especially that it’s possible to pass in a Function as a prop.

This forum is amazing! I’m really grateful to you all :smiley:

I ended up getting my desired syntax:

{{testVal}}
<once-group>
  <once-button :onClickFn="() => testVal='A'">A</once-button>
  <once-button :onClickFn="() => testVal='B'">B</once-button>
</once-group>

and a more complex usage

  <once-group>
    <once-button 
      :key="certainty"
      :value="certainty"
      v-for="certainty in certainties" 
      :onClickFn="submitCertainty">{{$root.renderAsPercent(certainty)}}</once-button>
  </once-group>

Like this:

Vue.component('once-group', {
  data: function(){
    return {
      userAnswer: null
    }
  },
  template: `<div><slot></slot></div>`,
  methods: {
    childClicked: function(onClickFn, value) {
      // Also provide default if onClickFn is not provided.
      var onClickFn  = onClickFn || this.$root.show;

      if (this.userAnswer === null){
        onClickFn(value);
      }
      this.userAnswer = value;

      var this_ = this;
      this.$slots.default.forEach((item, index) => {
        if (item.tag) {
          var button_value = item.componentInstance.$props.value;
          if (button_value === this_.userAnswer){
            item.elm.classList.add('selected-answer');
          } else {
            item.elm.setAttribute('disabled', true); 
          }
        }
      });

    }
  }
});

Vue.component('once-button', {
  props: {
    value: {type: [String, Number]},
    onClickFn: {type: Function},
  },
  template: '<button @click="$parent.childClicked(onClickFn, value)"><slot></slot></button>'
});