homer0 / svelte-extend

Create new Svelte components by extending existing ones
MIT License
25 stars 1 forks source link

about injecting html blocks #28

Closed chumager closed 1 year ago

chumager commented 1 year ago

Hi how are you? I've been looking for something like your plugin. I've a similar plugin for vue 2, but it allows to change some parts of the extended template, and the script changes are made with the extends attribute.

In my case I've a generic input base component, who does all what is needed to use a input, show data and it's details. Some of my components dos almost nothing, for example change the formatter option, convert the data, others changes everything, the input section, the section that shows the value and the detail section, but all keeps the same base logic.

Here are some examples: String.js: Here I only change some functions to set a different formatter and reaction to keypress

export default {
  extends: Vue.GenericInput,
  methods: {
    keypress(e) {
      this.input.onlyAlpha && !this.isAlpha(e.key, this.input.extraChars ?? " ") && e.preventDefault();
    },
    formatter(v) {
      const {input} = this;
      let f = v => v;
      let F;
      if (input.uppercase) f = v => `${v}`.toUpperCase();
      if (input.lowercase) f = v => `${v}`.toLowerCase();
      if (input.trim) F = v => `${f(v)}`.replace(/\s+/g, " ");
      else F = f;
      if (this.$attrs.formatter) return this.$attrs.formatter(F(v));
      return F(v);
    }
  }
};

TimeFrame.vue This one changes the way the data is visualized and the way the input is rendered, so I can display it as a mini form instead a simple input.

<template extends="./GenericInput.vue">
  <extensions>
    <extension point="View">
      <span>{{ humanize }}</span>
    </extension>
    <extension point="Input">
      <div
        :class="{'form-control': true, 'is-valid': input.state === true, 'is-invalid': input.state === false}"
        style="height: auto"
      >
        <itd-form v-model="innerValue" cols="1" :form="form" :config="{}" render-text="TimeFrame" />
      </div>
    </extension>
  </extensions>
</template>

<script>
//TODO controlar el mínimo y máximo
const {$validator} = Vue;
$validator.addValidator("TimeFrame", "default", function () {
  return this.value && (!this.value.duration || !this.value.measure)
    ? "La periodicidad debe tener medida de tiempo y duración"
    : "";
});
/*global cloneDeep, isNil, moment*/
import {mapGetters, mapState} from "vuex";
export default {
  $lodash: ["cloneDeep", "isNil"],
  extends: Vue.GenericInput,
  data() {
    let options = this.input?.measures?.reduce(
      (obj, measure) =>
        Object.assign(obj, {
          [measure.value]: Array(measure.duration)
            .fill()
            .map((__, idx) => idx + 1)
        }),
      {}
    );
    return {
      innerValue:
        typeof this.value === "object" && !isNil(this.value) ? cloneDeep(this.value) : {duration: null, measure: null},
      options
    };
  },
  computed: {
    ...mapGetters("Config", ["getLocale"]),
    ...mapState("Config", ["currentLanguage"]),
    humanize() {
      if (this.value) {
        this.currentLanguage;
        let {measure, duration} = this.value;
        const Measure = moment
          .duration(duration > 1 ? 2 : 1, measure)
          .locale(this.getLocale)
          .humanize({d: 7, w: 4})
          .split(" ")[1];
        return `${duration} ${Measure}`;
      }
      return "";
    },
    form() {
      //let {readonly, disabled} = this;
      const {localReadonly: readonly, localDisabled: disabled} = this;
      return [
        {
          name: "measure",
          label: "Medida",
          description: "Medida de tiempo",
          type: "select",
          component: "ItdSelect",
          options: [{text: "Seleccione la medida de tiempo", value: null, disabled: true}].concat(this.input.measures),
          readonly,
          disabled,
          state: !(!this.input.state && !this.innerValue.measure)
        },
        {
          name: "duration",
          label: "Duración",
          description: "Duración del periodo",
          tag: "b-select",
          component: "ItdSelect",
          options: [{text: "Seleccione la duración", value: null, disabled: true}].concat(
            this.innerValue?.measure ? this.options[this.innerValue.measure] : []
          ),
          readonly,
          disabled,
          state: !(!this.input.state && !this.innerValue.duration)
        }
      ];
    }
  },
  watch: {
    innerValue: {
      handler() {
        if (!this.form[1].options.includes(this.innerValue.duration)) this.innerValue.duration = null;
        this.$emit("input", this.innerValue);
      },
      deep: true
    }
  }
};
</script>

GenericInput.vue This is the base for all inputs and have a lot of sections that I can change according each component needs.

<template extendable>
  <!--eslint-disable vue/no-v-html-->
  <div :class="GIClass" :style="GIStyle">
    <template v-if="inputSwitch === 'tableView'">
      <extension-point name="View">
        <template v-if="!localLink">
          <span v-if="localLeft" v-html="localLeft" /><span v-html="formatter(tableViewValue)" /><span
            v-if="localRight"
            v-html="localRight"
          />
        </template>
        <b-link v-else @click="click">
          {{ $attrs["show-text"] || (tableViewValue ? tableViewDisplay : "") }}
          <!--b-icon
            v-if="$attrs['show-text'] || (tableViewValue ? tableViewDisplay : '')"
            icon="arrows-fullscreen"
          /-->
        </b-link>
      </extension-point>
    </template>
    <template v-if="inputSwitch === 'tableDetail'">
      <extension-point name="Detail">
        <pre v-html="localValue" />
      </extension-point>
    </template>
    <template v-if="inputSwitch === 'Input'">
      <extension-point name="preInput" />
      <extension-point name="Input">
        <b-input-group :size="localSize">
          <!--eslint-disable vue/no-v-text-v-html-on-component -->
          <b-input-group-prepend v-if="!plaintext" ref="prepend"
            ><extension-point name="prepend"><b-input-group-text v-if="localLeft" v-text="localLeft" /></extension-point
            ><template v-if="input.buttonLeft"
              ><b-btn @click="input.buttonLeft.click"
                ><icon v-if="input.buttonLeft.icon" :name="input.buttonLeft.icon" class="pt-1" /><span
                  v-else-if="input.buttonLeft.text"
                  v-html="input.buttonLeft.text"
                /><span v-else>???</span></b-btn
              ></template
            ></b-input-group-prepend
          >
          <!--eslint-enable vue/no-v-text-v-html-on-component -->
          <component
            :is="localTag"
            ref="input"
            v-model="localValue"
            class="input"
            :type="localType"
            :plaintext="plaintext"
            v-bind="{
              ...input,
              state: typeof $attrs.state === 'boolean' ? $attrs.state : input && input.state,
              ...attrs
            }"
            :lazy-formatter="lazyFormatter"
            :formatter="formatter"
            :lazy="localLazy"
            :size="localSize"
            :disabled="localDisabled"
            :readonly="localReadonly"
            :autocomplete="$attrs.autocomplete || 'off'"
            :placeholder="placeholder"
            @keypress.native="keypress($event)"
            @focus.native="$emit('focus', $event)"
            @blur.native="$emit('blur', $event)"
            v-on="listeners"
          />
          <!--eslint-disable vue/no-v-text-v-html-on-component -->
          <b-input-group-append v-if="!plaintext" ref="append"
            ><template v-if="input.buttonRight"
              ><b-btn :disabled="input.disabled" @click="input.buttonRight.click"
                ><icon v-if="input.buttonRight.icon" :name="input.buttonRight.icon" /><span
                  v-else-if="input.buttonRight.text"
                  v-html="input.buttonRight.text"
                /><span v-else>???</span></b-btn
              ></template
            ><extension-point name="append"
              ><b-input-group-text v-if="localRight" v-text="localRight" /></extension-point
          ></b-input-group-append>
          <!--eslint-enable vue/no-v-text-v-html-on-component -->
        </b-input-group>
      </extension-point>
      <extension-point name="postInput" />
    </template>
    <extension-point name="Extra" />
  </div>
</template>

<script>
/*global onDev, isEqual*/
function castBoolean(v) {
  if (typeof v === "boolean") return v;
  if (typeof v === "string") {
    switch (v.toLowerCase()) {
      case "true":
        return true;
      case "false":
        return false;
      default:
        return null;
    }
  }
}
export default {
  $lodash: "isEqual",
  $BVP: ["LinkPlugin", "InputGroupPlugin", "FormInputPlugin"],
  props: {
    value: {
      type: null,
      default: null
    },
    updateTableViewValue: {
      type: Boolean,
      default: false
    },
    //evaluar si es necesario sincronizar
    extra: {
      type: Object,
      default: () => ({})
    },
    row: {
      type: Object,
      default: () => ({})
    },
    field: {
      type: Object,
      default: () => ({})
    },
    input: {
      type: Object,
      default: () => ({})
    },
    tableView: {
      type: Boolean,
      default: false
    },
    tableDetail: {
      type: Boolean,
      default: false
    },
    disabled: {
      type: Boolean,
      default: false
    },
    readonly: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      localValue: null,
      plaintext: false,
      link: false,
      tableViewValue: null,
      tableViewDisplay: Array.isArray(this.value) ? this.value.length : "Ver",
      type: "text",
      left: "",
      rigth: "",
      attrs: {},
      field_attr: {},
      GIClass: "",
      GIStyle: "",
      altView: false,
      altDetail: false,
      altInput: false,
      placeholder: this.$attrs.placeholder || this.input.placeholder
    };
  },
  computed: {
    localLazy() {
      const {
        $attrs: {lazy, "no-lazy": noLazy}
      } = this;
      if ("lazy" in this) return this.lazy;
      if ("lazy" in this.$attrs) return lazy === "" ? true : castBoolean(lazy);
      if ("no-lazy" in this.$attrs) return noLazy === "" ? false : !castBoolean(noLazy);
      if ("lazy" in this.input) return this.input.lazy;
      if ("lazy" in (this.attrs ?? {})) return this.attrs.lazy;
      return this?.input?.filter ? false : true;
    },
    inputSwitch() {
      const {tableView, tableDetail} = this;
      if (tableView) return "tableView";
      if (tableDetail) return "tableDetail";
      return "Input";
    },
    //el type varia si es filtro o no y si el padre lo pide distinto
    localType() {
      //if ("type" in this.$attrs) return this.$attrs.type;
      const {filter = false, filterType} = this.input || {};
      return filter ? filterType || "search" : this.type;
    },
    localSize() {
      return this.$attrs.size || this.size || this.input.size || "sm";
    },
    //no hay casos donde el extends necesite poner disabled por ahora
    localDisabled() {
      const {disabled, input} = this;
      return input.disabled || disabled;
    },
    //no hay casos donde el extends necesite poner readonly por ahora
    localReadonly() {
      const {readonly, input} = this;
      return readonly || input.readonly;
    },
    //este estado siempre lo define el padre y ahora se exige kebab-case
    localClick() {
      let {click} = this;
      return click ?? (() => this.$emit("click"));
    },
    localLink() {
      //esto no tiene sentido que venga desde el input
      return "link" in this.$attrs || this.link;
    },
    localTag() {
      return this.$attrs.tag || this.input.tag || this.tag || "b-input";
    },
    lazyFormatter() {
      return this.attrs.lazyFormatter ?? this.$attrs.lazyFormatter ?? this.input.lazyFormatter ?? false;
    },
    localLeft() {
      //esto puede venir como attr, data o input
      return this.$attrs.left || this.left || this.input.left || this.field.left || "";
    },
    localRight() {
      return this.$attrs.right || this.right || this.input.right || this.field.right || "";
    },
    listeners() {
      const {input, change, ...listeners} = this.$listeners; //eslint-disable-line no-unused-vars
      return listeners;
    }
  },
  watch: {
    field_attr: {
      handler() {
        const newField = {...this.field, ...this.field_attr};
        if (!isEqual(this.field, newField)) {
          console.debug("field_attr", newField);
          this.$emit("update:field", newField);
        }
      },
      deep: true,
      immediate: true
    },
    value: {
      handler(v) {
        let res = this.initVal(v);
        if (!isEqual(this.initVal(this.localValue), res)) {
          this.localValue = res;
        }
      }
    },
    localValue: {
      handler(val, old) {
        if (!isEqual(val, old)) {
          let res = this.emitter(val);
          if (this.updateTableViewValue) this.tableViewValue = this.initVal(this.localValue);
          this.$emit("input", res);
        }
      },
      deep: true
    },
    plaintext(v) {
      if (v) this.localValue = this.plainformat(this.localValue);
    }
  },
  created() {
    this.localValue = this.tableViewValue = this.initVal(this.value);
  },
  mounted() {
    if (onDev) {
      window.GenericInput = window.GenericInput || {};
      this.input && (window.GenericInput[this.input.name] = this);
    }
  },
  methods: {
    //methodos de verificación de datos
    isNumber(v) {
      return /^\d$/.test(v);
    },
    isAlphaNumeric(v) {
      return /^\w$/.test(v);
    },
    isAlpha(v, extra) {
      return new RegExp(`^[\\p{L}${extra || ""}]+$`, "u").test(v);
    },
    //??? posiblemente redundante
    click() {
      console.debug("CLICK", this);
      this.$emit("click");
    },
    //administración interna de datos iniciales y a entregar
    initVal(v) {
      let f = v => v;
      f = this.$attrs.initVal || f;
      return f(v);
    },
    plainformat(v) {
      let f = v => v;
      f = this.$attrs.plainformat || f;
      return f(v);
    },
    formatter(v) {
      let f = v => v;
      f = this.$attrs.formatter || f;
      return f(v);
    },
    emitter(v) {
      let f = v => v;
      f = this.$attrs.emitter || f;
      return f(v);
    },
    keypress(v) {
      let f = () => true;
      f = this.$attrs.keypress || f;
      //encapsulamos f para soportar Delete y Backspace
      let res = v => ["Backspace", "Delete"].includes(v.key) || f(v);
      if (!res(v)) v.preventDefault();
    }
  }
};
</script>

Do you think is there anyway con accomplish what I need with your module?

The plugin repository is https://github.com/mrodal/vue-inheritance-loader

Thanks in advance.

homer0 commented 1 year ago

Hi @chumager 👋

Sorry, but I consider this lib to be feature-done, and I don't plan on adding new features to it.

Like I mentioned in the README's disclaimer: I built this for a very specific case I had, but I believe it's not a good idea.

Now, in case you want to fork and extend, or build your own thing, I read your code and a little bit of the repo you linked and:

Hope that helps!