Saturday, 5 October 2019

Bind vuex state and mutations to checkbox component properties in TypeScript-based Vue

Problem

Create the checkbox as Vue component, herewith:

  1. No logic inside checkbox component allowed: all event handlers and also checked property are fully depends on external logic, which could be the vuex store.
  2. We should not watch the checkbox "checked" state: checked it or not, it depends on, again, external logic, e. g. vuex state or getter.

Try 1

Concept

The checkbox component has checked and onClick properties, which value are off course, could be dynamic.

Component

Template in Pug language:

label.SvgCheckbox-LabelAsWrapper(:class="rootElementCssClass" @click.prevent="onClick")
  input.SvgCheckbox-InvisibleAuthenticCheckbox(
    type="checkbox"
    :checked="checked"
    :disabled="disabled"
  )
  svg(viewbox='0 0 24 24').SvgCheckbox-SvgCanvas
    path(
      v-if="!checked"
      d='M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M19,5V19H5V5H19Z'
    ).SvgCheckbox-SvgPath.SvgCheckbox-SvgPath__Unchecked
    path(
      v-else
      d='M10,17L5,12L6.41,10.58L10,14.17L17.59,6.58L19,8M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z'
    ).SvgCheckbox-SvgPath.SvgCheckbox-SvgPath__Checked
  span(v-if="text").SvgCheckbox-AppendedText 
import { Vue, Component, Prop } from 'vue-property-decorator';

@Component
export default class SimpleCheckbox extends Vue {

  @Prop({ type: Boolean, required: true }) private readonly checked!: boolean;

  @Prop({ type: Boolean, default: false }) private readonly disabled!: boolean;

  @Prop({ type: String }) private readonly text?: string;
  @Prop({ type: String }) private readonly parentElementCssClass?: string;

  @Prop({ type: Function, default: () => {} }) private readonly onClick!: () => void;
}

Store module

import { VuexModule, Module, Mutation } from "vuex-module-decorators";
import store, { StoreModuleNames } from "@Store/Store";


@Module({ name: StoreModuleNames.example, store, dynamic: true, namespaced: true })
export default class ExampleStoreModule extends VuexModule {

  private _doNotPreProcessMarkupEntryPointsFlag: boolean = true;

  public get doNotPreProcessMarkupEntryPointsFlag(): boolean {
    return this._doNotPreProcessMarkupEntryPointsFlag;
  }

  @Mutation
  public toggleDoNotPreProcessMarkupEntryPointsFlag(): void {
    this._doNotPreProcessMarkupEntryPointsFlag = !this._doNotPreProcessMarkupEntryPointsFlag;
  }
}

Usage

SimpleCheckbox(
  :checked="relatedStoreModule.doNotPreProcessMarkupEntryPointsFlag"
  :onClick="relatedStoreModule.toggleDoNotPreProcessMarkupEntryPointsFlag"
  parentElementCssClass="RegularCheckbox"
)
import { Component, Vue } from "vue-property-decorator";
import { getModule } from "vuex-module-decorators";
import ExampleStoreModule from "@Store/modules/ExampleStoreModule";
import template from "@Templates/ExampleTemplate.pug";
import SimpleCheckbox from "@Components/Checkboxes/MaterialDesign/SimpleCheckbox.vue";

@Component({ components: { SimpleCheckbox } })
export default class MarkupPreProcessingSettings extends Vue {
  private readonly relatedStoreModule: ExampleStoreModule = getModule(ExampleStoreModule);
}

Warings

Appears if click the checkbox. Checkbox works as we need, but some of Vue concept has been violated.

enter image description here

vue.common.dev.js:630 [Vue warn]: $attrs is readonly.

found in

---> <SimpleCheckbox> at hikari-frontend/UiComponents/Checkboxes/MaterialDesign/SimpleCheckbox.vue
       <MarkupPreProcessingSettings>
         <Application> at ProjectInitializer/ElectronRendererProcess/RootComponent.vue
           <Root>

vue.common.dev.js:630 [Vue warn]: $listeners is readonly.

found in

---> <SimpleCheckbox> at hikari-frontend/UiComponents/Checkboxes/MaterialDesign/SimpleCheckbox.vue
       <MarkupPreProcessingSettings>
         <Application> at ProjectInitializer/ElectronRendererProcess/RootComponent.vue
           <Root>

vue.common.dev.js:630 [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "checked"

found in

---> <SimpleCheckbox> at hikari-frontend/UiComponents/Checkboxes/MaterialDesign/SimpleCheckbox.vue
       <MarkupPreProcessingSettings>
         <Application> at ProjectInitializer/ElectronRendererProcess/RootComponent.vue
           <Root>

Musings

This warning are being emitted frequent cause is new value to some vue-property has been assign inside component. Explicitly, I did not the manipulations like this.

The problem is in :onClick="relatedStoreModule.toggleDoNotPreProcessMarkupEntryPointsFlag". It looks like it compiles to something like <component>.$props.onClick="<vuex store manipulations ...>" - if it so, it is implicit property mutation inside component.

Try 2

Concept

Based Vue documentation, Customizing Component section:

Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
})

The equivalent for TypeScript with vue-property-decorator will be:

import { Vue, Component, Model } from 'vue-property-decorator'

@Component
export default class YourComponent extends Vue {
  @Model('change', { type: Boolean }) readonly checked!: boolean
}
Component
label.SvgCheckbox-LabelAsWrapper(:class="rootElementCssClass")
  input.SvgCheckbox-InvisibleAuthenticCheckbox(
    type="checkbox"
    :checked="checked"
    :disabled="disabled"
    @change="$emit('change', $event.target.checked)"
  )
  svg(viewbox='0 0 24 24').SvgCheckbox-SvgCanvas
    // ...
import { Vue, Component, Prop, Model } from "vue-property-decorator";

@Component
export default class SimpleCheckbox extends Vue {

  @Model('change', { type: Boolean }) readonly checked!: boolean;

  @Prop({ type: Boolean, default: false }) private readonly disabled!: boolean;

  @Prop({ type: String }) private readonly text?: string;
  @Prop({ type: String }) private readonly rootElementCssClass?: string;
}

Usage

SimpleCheckbox(
  v-model="doNotPreProcessMarkupEntryPointsFlag"
  rootElementCssClass="RegularCheckbox"
)

In TypeScript, to use the v-model, we need to declare getter and same-name setter:

@Component({
  template,
  components: {
    SimpleCheckbox,
    // ...
  }
})
export default class MarkupPreProcessingSettings extends Vue {

  private readonly relatedStoreModule: MarkupPreProcessingSettingsStoreModule =
      getModule(MarkupPreProcessingSettingsStoreModule);
  //...
  private get doNotPreProcessMarkupEntryPointsFlag(): boolean {
    return this.relatedStoreModule.doNotPreProcessMarkupEntryPointsFlag;
  }

  private set doNotPreProcessMarkupEntryPointsFlag(_newValue: boolean) {
    this.relatedStoreModule.toggleDoNotPreProcessMarkupEntryPointsFlag();
  }
}

Warnings

Same errors set:

enter image description here

Limitations

First, we need to create new getter and setter in Vue Component class. It will be cool if possible to avoid id. Unfortunately, for vuex class (by vuex-module-decorators), TypeScript setters are not available, we need use the @Mutation-decorated method instead.

Also, this solution will not work for elements rendered by v-for. It make this solution useless.

Try 3

Concept

Event emitter and custom event listener usage. This solution also works properly, but Vue emits warnings.

Component

label.SvgCheckbox-LabelAsWrapper(:class="rootElementCssClass" @click.prevent="$emit('toggled')")
  // ...

Usage

SimpleCheckbox(
  :checked="relatedStoreModule.doNotPreProcessMarkupEntryPointsFlag"
  @toggled="relatedStoreModule.toggleDoNotPreProcessMarkupEntryPointsFlag"
  rootElementCssClass="RegularCheckbox"
)

Warnings

enter image description here


Update

There are some riddles left yet, however problem has been solved. See my answer below.



from Bind vuex state and mutations to checkbox component properties in TypeScript-based Vue

No comments:

Post a Comment