mrodal / vue-inheritance-loader

Webpack loader to be used along with vue-loader for Single File Components that provides template extension
38 stars 5 forks source link

render conditionally #13

Closed chumager closed 4 years ago

chumager commented 4 years ago

Hi, how can I conditionally render an extension point?

AFAIK the mere existence of <extension point="x">, even if its empty, will render the content, so if I've a inner tag with v-if directive it'll render nothing if the condition is not accomplished.

the v-if directive does nothing in the <extension> tag, so I don't know how to render conditionally preserving the <extension-point> if the condition is false

EDIT: Finally I realize you can wrap the <extension> in a <template> with v-if directive, but it's not in the Readme and it shows like you can only use extensions and extension tags inside the template...

Eitherway it'll be nice to hace at least the v-if directive in the extension tag.

Best regards.

chumager commented 4 years ago

Hi, my mistake... internally I changed the component, the <template v-if...> wrapping the <extension point...> doesn't work.

Any chance to fix this somehow???

Best Regards...

mrodal commented 4 years ago

Could you provide an example with what you want to achieve?

chumager commented 4 years ago

Sure... I've a component who shows 3 options, when is called to view data, when is called to view the detail's data and when is called to provide an input for the data type.

One of de extended component (a select) will use the default in some cases and in other it'll use another content.

So the base template is:

<template extendable>
  <div :class="GIClass" :style="GIStyle">
    <extension-point name="Extra" />
    <template v-if="inputSwitch === 'tableView'">
      <template v-if="altView">
        <extension-point name="altView">
          <span>Debe agregar el extensión point altView, ya que la variable altView tiene valor {{altView}}</span>
        </extension-point>
      </template>
      <template v-else>
        <extension-point name="View">
          <span v-if="!localLink">{{ localLeft }}{{ formatter(tableViewValue) }}{{ localRight }} </span>
          <b-link
            v-else
            @click="
              row.toggleDetails();
              localClick();
            "
            v-text="
              row.detailsShowing
                ? $attrs['hide-text'] || 'Ocultar'
                : $attrs['show-text'] || (tableViewValue ? tableViewDisplay : '')
            "
          />
        </extension-point>
      </template>
    </template>
    <template v-if="inputSwitch === 'tableDetail'">
      <h2>{{ field.detail || field.label || field.key }}</h2>
      <template v-if="altDetail">
        <extension-point name="altDetail">
          <span>Debe agregar el extensión point altDetail, ya que la variable altDetail tiene valor {{altDetail}}</span>
        </extension-point>
      </template>
      <template v-else>
        <extension-point name="Detail">
          <pre v-html="localValue" />
        </extension-point>
      </template>
    </template>
    <template v-if="inputSwitch === 'Input'">
      <template v-if="altInput">
        <extension-point name="altInput">
          <span>Debe agregar el extensión point altInput, ya que la variable altInput tiene valor {{altInput}}</span>
        </extension>
      </template>
      <template v-else>
      <extension-point name="Input">
        <b-input-group :prepend="localLeft" :append="localRight" :size="localSize">
          <b-input-group-prepend>
            <extension-point name="prepend" />
          </b-input-group-prepend>
          <component
            :is="localTag"
            ref="input"
            class="input"
            v-model="localValue"
            :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"
            @keypress.native="keypress($event)"
            @focus.native="$emit('focus', $event)"
            @blur.native="$emit('blur', $event)"
            v-on="listeners"
            :disabled="localDisabled"
            :readonly="localReadonly"
            :autocomplete="$attrs.autocomplete || 'off'"
          />
          <b-input-group-append v-if="!plaintext">
            <extension-point name="append" />
          </b-input-group-append>
        </b-input-group>
        <extension-point name="postInput" />
      </extension-point>
      </template>
    </template>
  </div>
</template>

As you can see the default content is too complex to copy&paste and it'll be no sense to use this loader in case I've to do it...

In the example it's also shown that I fixed the problems using conditional templates to wrap two extension-point so my select component can do this change by using an internal data to accomplish the behavior I want.

Because of this conditions the component looses simplicity and forces to know the inner logic, and that's exactly what I want to avoid

One of the extended components:

<template extends="./GenericInput.vue">
  <extensions>
    <extension point="altView">
      <simple-name :_id="value" :field="field" :row="row" table-view @click="click" />
    </extension>
    <extension point="altDetail">
      <simple-name :_id="value" :field="field" :row="row" table-detail />
    </extension>
    <extension point="Detail">
      <ul>
        <li v-for="(myval, id) in localValue" :key="id">
          <span v-text="detailValue(myval)" />
        </li>
      </ul>
    </extension>
    <extension point="altInput">
      <type-ahead v-model="localValue" v-bind="{extra, input, disabled, readonly}" />
    </extension>
  </extensions>
</template>
<script>
/*global _, ci*/
import GenericInput from "./GenericInput";
export default {
  extends: GenericInput,
  components: ci(["SimpleName", "TypeAhead"]),
  data() {
    return {
      tag:
        this.input.tag || (this.input?.options?.length || this.field?.options?.length) > 4
          ? "b-select"
          : this.input.multiple
          ? "b-check-group"
          : "b-radio-group",
      link: Array.isArray(this.value) || this.field.ref || this.field.refPath,
      tableViewValue: Array.isArray(this.value) ? !_.isEmpty(this.value) : this.viewValue(),
      tableViewDisplay: Array.isArray(this.value) ? this.value.length : this.value,
      GIStyle: Array.isArray(this.value) && this.tableView ? {"text-align": "right"} : {},
      altView: this.Ref(),
      altDetail: this.Ref(),
      altInput: this.RefI() && !this.input.multiple,
      attrs: {
        buttonVariant: "outline-primary",
        buttons: true,
        ...this.input.attrs,
      },
    };
  },
  methods: {
    Ref() {
      const {ref, refPath} = this.field;
      return !!(ref || refPath);
    },
    RefI() {
      const {ref, refPath} = this.input;
      return !!(ref || refPath);
    },
    viewValue() {
      const {options} = this.field;
      return options?.find((el) => el.value === this.value)?.text || this.value;
    },
    detailValue(v) {
      const {options} = this.field;
      return options?.find((el) => el.value === v)?.text || v;
    },
  },
};
</script>

Finally I've to overwrite the altView, altDetail and altInput so the base component knows it has to use other extension-point...

That's break all the logic of having tag to replace in the extended component.

Tell me if that's a clear example.

Best regards.

PS: I was looking your code and it seems you just do a inline replacement between the extension and extension point, and wrapping with a template, so it seems to be hard to do what I need, at that point you don't have access to the component data so you can't do any analysis, but what you can do is: "if you find and extension with v-if you can keep the extension point and add a v-else" that way your transformation will keep the v-if logic and in case de logic returns false the base extension-point will be shown... do I make myself clear??? if not I can explain better.

mrodal commented 4 years ago

Do you have multiple components that extend GenericInput besides the one you shared?

And did you try wrapping the content of the <extension> in a template with v-if? Like this:

<extension point="altView">
    <template v-if="altView">
        <simple-name :_id="value" :field="field" :row="row" table-view @click="click" />
    </template>
</extension>
chumager commented 4 years ago

Hi, I've like 20 components that extends GenericInput. I've changed the architecture so now every data type has it's own component. I've some standard ones like, string, text, number, percentage or currency, but some others weird.

I don't want to use "altView", Just "View" with condition to render. So it'll only render if the condition is true, and if not, then keep the extension-point.

The existense of "altView" is because I've no way to keep the extension point conditionally. If I use your example, the extension point will be changed for nothing, instead of keeping the content.

As I told you before the only way I think this could be accomplished is by searching for a v-if attribute in the extension tag and if it exists, then instead o replacing the extension point just add the extension with the v-if and keep below the extension-point and add a v-else directive.

something like: BaseComponent.vue

<template extendable>
  ...
  <extension-point name="x">
    ...
  </extension-point>
</template>

ExtendedComponent

<template extends="./BaseComponent">
  <extensions>
    <extension point="x" v-if="something">
      ...
    </extension>
  </extensions>
</template>

will render:

<template>
  ...
  <template v-if="something"><!-- the extension -->
    ...
  </template>
  <template v-else><!-- the extension-point-->
  ...
  </template>
</template>

As an example, there is several ways to render a select input, if it's simple, then it can be rendered like radio group, select or group of buttons. if it's multiple can be rendered like checkbox group, select multiple or group of buttons. all those can be done just by changing the "tag" attribute, because the inner logic it's the same. But if it has lots of options (hundreds), then all the previous options are messy, so in that case I want to change the input component for another with it's own inner logic (it's just an example, my problems are greater than that).

Just looking your code, it looks like you only change the source, but you don't compile the code, so when you make the change:

https://github.com/mrodal/vue-inheritance-loader/blob/31298a912ea16cee7f70a05006f81fefa1dde2a7/src/index.js#L137-L143

there you can add a child (if it has v-if) instead of replacing it and add the v-else attribute to the extension-point.

Do you get my point?

It'll change the actual logic "if I define a extension then I always want to render the extension instead of extension-point", to "if I define a extension conditionally then I want to render that extension in the case the result is true, otherwise I want to keep the extension-point"

IMHO it's not a hard change and will add tremendous potentiality to your loader.

Best regards...

mrodal commented 4 years ago

What I said earlier, but now using your example:

BaseComponent.vue

<template extendable>
  ...
  <extension-point name="x">
    ...
  </extension-point>
</template>

ExtendedComponent with the v-if in the template works for me. Do you see a difference between having it in the template and on the <extension>?

<template extends="./BaseComponent">
  <extensions>
    <extension point="x">
        <template v-if="something">
            ...
        </template>
    </extension>
  </extensions>
</template>
chumager commented 4 years ago

well... according to your loader if <extension point="x"> exists, then <extension-point name="x"> will be replaced, so what's going on is that I'll always have a <template v-if...> alone for that component.

image

there is lots of different selects, Empresa, Proyecto, Simulación, are special cases, Estado and Tipo de Simulación, are normal cases, both are empty because in the extended component the "view" part now has

      <template v-if="altView">
        <simple-name :_id="value" :field="field" :row="row" table-view @click="click" />
      </template>

Because the loader replaces the "default" part by this one, so only special cases are rendered, my "default case" now is empty. This is how mi GenericInput component (rendered) looks (just changing the "View" part):

<template>

<div :class="GIClass" :style="GIStyle">
  <template></template>
  <template v-if="inputSwitch === 'tableView'">
    <template>
      <span v-if="!localLink">{{ localLeft }}{{ formatter(tableViewValue) }}{{ localRight }} </span>
      <b-link v-else @click="
          row.toggleDetails();
          localClick();
        " v-text="
          row.detailsShowing
            ? $attrs['hide-text'] || 'Ocultar'
            : $attrs['show-text'] || (tableViewValue ? tableViewDisplay : '')
        "></b-link>
    </template>
  </template>
  <template v-if="inputSwitch === 'tableDetail'">
    <h2>{{ field.detail || field.label || field.key }}</h2>
    <template v-if="altDetail">
      <template>
        <span>Debe agregar el extensión point altDetail, ya que la variable altDetail tiene valor {{altDetail}}</span>
      </template>
    </template>
    <template v-else>
      <template>
        <pre v-html="localValue"></pre>
      </template>
    </template>
  </template>
  <template v-if="inputSwitch === 'Input'">
    <template v-if="altInput">
      <template>
        <span>Debe agregar el extensión point altInput, ya que la variable altInput tiene valor {{altInput}}</span>

    </template></template>
    <template v-else>
    <template>
      <b-input-group :prepend="localLeft" :append="localRight" :size="localSize">
        <b-input-group-prepend v-if="!plaintext" ref="prepend">
          <template></template>
        </b-input-group-prepend>
        <component :is="localTag" ref="input" class="input" v-model="localValue" :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" @keypress.native="keypress($event)" @focus.native="$emit('focus', $event)" @blur.native="$emit('blur', $event)" v-on="listeners" :disabled="localDisabled" :readonly="localReadonly" :autocomplete="$attrs.autocomplete || 'off'"></component>
        <b-input-group-append v-if="!plaintext" ref="append">
          <template></template>
        </b-input-group-append>
      </b-input-group>
      <template></template>
    </template>
    </template>
  </template>
</div>

                  </template> 

And his is how mi select component looks like now (with your change).

<template>

<div :class="GIClass" :style="GIStyle">
  <template></template>
  <template v-if="inputSwitch === 'tableView'">
    <template>
    <template v-if="altView">
      <simple-name :_id="value" :field="field" :row="row" table-view @click="click"></simple-name>
    </template>
  </template>
  </template>
  <template v-if="inputSwitch === 'tableDetail'">
    <h2>{{ field.detail || field.label || field.key }}</h2>
    <template v-if="altDetail">
      <template>
    <simple-name :_id="value" :field="field" :row="row" table-detail></simple-name>
  </template>
    </template>
    <template v-else>
      <template>
    <ul>
      <li v-for="(myval, id) in localValue" :key="id">
        <span v-text="detailValue(myval)"></span>
      </li>
    </ul>
  </template>
    </template>
  </template>
  <template v-if="inputSwitch === 'Input'">
    <template v-if="altInput">
      <template>
    <type-ahead v-model="localValue" v-bind="{extra, input, disabled, readonly}"></type-ahead>
  </template></template>
    <template v-else>
    <template>
      <b-input-group :prepend="localLeft" :append="localRight" :size="localSize">
        <b-input-group-prepend v-if="!plaintext" ref="prepend">
          <template></template>
        </b-input-group-prepend>
        <component :is="localTag" ref="input" class="input" v-model="localValue" :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" @keypress.native="keypress($event)" @focus.native="$emit('focus', $event)" @blur.native="$emit('blur', $event)" v-on="listeners" :disabled="localDisabled" :readonly="localReadonly" :autocomplete="$attrs.autocomplete || 'off'"></component>
        <b-input-group-append v-if="!plaintext" ref="append">
          <template></template>
        </b-input-group-append>
      </b-input-group>
      <template></template>
    </template>
    </template>
  </template>
</div>

                  </template> 

As You can see, the inner code in the "View" segment is always replaced.

mrodal commented 4 years ago

Sorry, I just now understood the issue, it was hard to follow with so much code

I published a new version, 0.2.1, and now you have a new tag, <extension-super/>, which you can use to invoke the parent's code.. going again with your example, you can do this:

BaseComponent.vue

<template extendable>
  ...
  <extension-point name="x">
    ...
  </extension-point>
</template>

ExtendedComponent with the v-if in the template works for me. Do you see a difference between having it in the template and on the <extension>?

<template extends="./BaseComponent">
  <extensions>
    <extension point="x">
        <template v-if="something">
            ...
        </template>
        <template v-else>
            <extension-super/>
        </template>   
    </extension>
  </extensions>
</template>

I think this way is more powerful and more clear than transforming the v-if in the extension to 2 templates with v-if and v-else with the default code. This way is also similar to calling the parent method on OOP.

Let me know if this works for you

chumager commented 4 years ago

Hi, that'll do the job... it's more verbose but more readable, my way it simpler but more obscure, so I think your choice it's better...

Thanks a lot for the effort, the first try (lazy one with v-if in the tags) didn't work, but the extended one (with templates wrapping) works perfect...

Best regards...