Binding sb-asset-selector

I’m working on a plugin which allows content editors to control basic styling of content within storyblok. It’s derived from the Open Hours plugin example with similar data structure, where: instead of days there are hardcoded breakpoints (xsmall, small, medium, large, and xlarge) and instead of hour segments there are style rule key/value pairs.

Here’s a screenshot of what it looks like:

Basic string key/value pairs are easy enough to extrapolate and it works fine. However, working with image style properties (e.g. background-image) is not user friendly at all so I was trying to use sb-asset-selector instead of just a textbox for image selection. The problem is sb-asset-selector expects a field prop which I can’t figure out how to align to my model structure.

For now, I have it set up naively just checking if the attribute name ends with -image and then showing the sb-asset-selector instead of the input. This of course would change assuming I can make the sb-asset-selector work as I’m hoping.

image

Below is the plugin code so far. It functions as expected for non-asset fields but again it doesn’t bind correctly for those that show the asset selector. Is there’s any way to deep bind the sb-asset-selector such that the selected asset URL will populate in the rule’s value field?

<template>
  <div class="style-editor">
    <div
      v-for="(breakpoint, index) in model.breakpoints"
      :key="'bp' + index"
      class="StyleEditor__day"
    >
      <h4 class="StyleEditor__day-name">
        {{ breakpoint.displayName }}
      </h4>
      <ol class="StyleEditor__list uk-margin-top-remove uk-margin-bottom-remove">
        <li
          v-for="(rule, ruleIndex) in breakpoint.rules"
          :key="'r' + ruleIndex"
          class="StyleEditor__list-item uk-flex uk-flex-middle"
        >
          <input
            v-model="model.breakpoints[index].rules[ruleIndex].attribute"
            aria-label="CSS Attribute"
            class="uk-form-small uk-width-1-1"
            placeholder="Attribute"
          >
          <span class="StyleEditor__separator">
            :
          </span>
          <input
            v-if="!useFileEditor(index, ruleIndex)"
            v-model="model.breakpoints[index].rules[ruleIndex].value"
            aria-label="Value"
            class="uk-form-small uk-width-1-1"
            placeholder="Value"
          >
          <sb-asset-selector
            v-else
            :uid="uid"
            :field="'breakpoints[' + index + '].rules[' + ruleIndex + '].value'"
          ></sb-asset-selector>
          <a
            class="assets__item-trash"
            aria-label="Remove item"
            @click="removeFields(index, ruleIndex)"
          >
            <i class="uk-icon-minus-circle" />
          </a>
        </li>
      </ol>
      <a
        class="blok__full-btn uk-margin-small-top"
        @click="addFields(index)"
      >
        <i class="uk-icon-plus-circle uk-margin-small-right" />
        Add fields
      </a>
    </div>
  </div>
</template>

<script>
export default {
  mixins: [window.Storyblok.plugin],
  watch: {
    model: {
      deep: true,
      handler(value) {
        // Let Storyblok know that the value was updated.
        this.$emit("changed-model", value);
      }
    }
  },
  methods: {
    addFields(index) {
      this.model.breakpoints[index].rules.push({
        attribute: "",
        value: ""
      });
    },
    removeFields(breakpointIndex, ruleIndex) {
      this.model.breakpoints[breakpointIndex].rules = this.model.breakpoints[
        breakpointIndex
      ].rules.filter((_, i) => i !== ruleIndex);
    },
    useFileEditor(index, ruleIndex) {
      var attr = this.model.breakpoints[index].rules[ruleIndex].attribute;

      if (attr.includes(":")) {
        attr = attr.split(":")[0];
      }

      return attr.trim().endsWith("-image");
    },
    initWith() {
      return {
        breakpoints: [
          {
            name: "xsmall",
            displayName: "Extra Small",
            minWidth: 0,
            rules: []
          },
          {
            name: "small",
            displayName: "Small",
            minWidth: 321,
            rules: []
          },
          {
            name: "medium",
            displayName: "Medium",
            minWidth: 641,
            rules: []
          },
          {
            name: "large",
            displayName: "Large",
            minWidth: 1025,
            rules: []
          },
          {
            name: "xlarge",
            displayName: "Extra Large",
            minWidth: 1441,
            rules: []
          }
        ],
        // This is the name of our plugin.
        plugin: "style-editor"
      };
    }
  }
};
</script>

<style>
.StyleEditor__day + .StyleEditor__day {
  margin-top: 10px;
}

.StyleEditor__day-name {
  margin-bottom: 5px;
}

.StyleEditor__list {
  padding-left: 0;
}

.StyleEditor__separator {
  margin-right: 4px;
  margin-left: 4px;
}

.StyleEditor__list-item + .StyleEditor__list-item {
  margin-top: 5px;
}
</style>

Hi, I spoke with our dev team about your question and currently it is not possible to pass a nested field. I would recommend you to create a new Feature Request named “Possibility to pass nested field to sb-asset-selector component”, where would you describe the issue/problem/feature.

Here is the link for github repo where you can create the FR :wink: https://github.com/storyblok/storyblok/issues

Thank you Samuel. In the meantime, and I know this is a stretch, would it be possible to get the source code for the sb-asset-selector component so I can fork it with the behavior I’m looking for? As I understand it, the component simply interacts with the management API to list the assets so I wouldn’t imagine there’s anything proprietary in there but it would serve as a good example for some advanced field type use cases which are otherwise missing in your documentation. I will of course include this request in the FR also.

Thanks again.

EDIT: Here’s the feature request filed earlier today based on this question: https://github.com/storyblok/storyblok/issues/345

1 Like

I am going to ask for it and get back to you soon.

I am sorry, but there is nothing we can share. The functionality is deeply connected with our frontend and the component is right now more less only the button.

I was able to figure out a hacky interim solution by looking through fieldtype-wrapper.js. It’s not perfect and it could stop working if there are any changes to the way the plugin frame communicates with the host window - but it does work and provides an improved user experience for our editors as we wait for the feature request to be completed.

Here’s what the plugin looks like for now in case anyone is curious or has similar requirements. As an added bonus, it includes an editor for color styles also. I’m happy to share my workspace if anyone is interested. Otherwise, just starting with the storyblok-fieldtype repo, pasting in the code below into Plugin.vue and yarn add lodash.set, should get you to the same place.

<template>
  <div class="style-editor">
    <div
      v-for="(breakpoint, index) in model.breakpoints"
      :key="'bp' + index"
      class="StyleEditor__day"
    >
      <h4 class="StyleEditor__day-name">
        {{ breakpoint.displayName }}
      </h4>
      <ol class="StyleEditor__list uk-margin-top-remove uk-margin-bottom-remove">
        <li
          v-for="(rule, ruleIndex) in breakpoint.rules"
          :key="'r' + ruleIndex"
          class="StyleEditor__list-item uk-flex uk-flex-middle uk-inline"
        >
          <input
            v-model="model.breakpoints[index].rules[ruleIndex].attribute"
            aria-label="CSS Attribute"
            class="uk-form-small uk-width-1-1"
            placeholder="Attribute"
          >
          <span class="StyleEditor__separator">
            :
          </span>
          <input
            v-model="model.breakpoints[index].rules[ruleIndex].value"
            aria-label="Value"
            class="uk-form-small uk-width-1-1"
            placeholder="Value"
          >
          <a
            v-if="useAssetSelector(index, ruleIndex)"
            class="assets__item-trash uk-icon-cog"
            aria-label="Select Asset"
            @click="showAssetSelector(index, ruleIndex)"
          ></a>
          <label
            v-else-if="useColorSelector(index, ruleIndex)"
            class="assets__item-trash uk-icon-cog"
            style="cursor: pointer"
            aria-label="Select Color"
          >
            <input
              v-model="model.breakpoints[index].rules[ruleIndex].value"
              type="color"
              style="display: none"
            >
          </label>
          <span
            v-else
            style="pointer-events: none"
            class="assets__item-trash uk-icon-hover uk-icon-ban"
          ></span>
          <a
            class="assets__item-trash uk-icon-minus-circle"
            aria-label="Remove item"
            @click="removeFields(index, ruleIndex)"
          ></a>
        </li>
      </ol>
      <a
        class="blok__full-btn uk-margin-small-top"
        @click="addFields(index)"
      >
        <i class="uk-icon-plus-circle uk-margin-small-right" />
        Add fields
      </a>
    </div>
  </div>
</template>

<script>
import _set from "lodash.set";

export default {
  mixins: [window.Storyblok.plugin],
  watch: {
    model: {
      deep: true,
      handler(value) {
        // Let Storyblok know that the value was updated.
        this.$emit("changed-model", value);
      }
    }
  },
  methods: {
    addFields(index) {
      this.model.breakpoints[index].rules.push({
        attribute: "",
        value: ""
      });
    },
    removeFields(breakpointIndex, ruleIndex) {
      this.model.breakpoints[breakpointIndex].rules = this.model.breakpoints[
        breakpointIndex
      ].rules.filter((_, i) => i !== ruleIndex);
    },
    useColorSelector(index, ruleIndex) {
      var attr = this.model.breakpoints[index].rules[ruleIndex].attribute;

      if (attr.includes(":")) {
        attr = attr.split(":")[0];
      }

      return attr.trim() === "color" || attr.trim().endsWith("-color");
    },
    useAssetSelector(index, ruleIndex) {
      var attr = this.model.breakpoints[index].rules[ruleIndex].attribute;

      if (attr.includes(":")) {
        attr = attr.split(":")[0];
      }

      return attr.trim().endsWith("-image");
    },
    showAssetSelector(index, ruleIndex) {
      // Determine the message targetOrigin
      const urlParams = new URLSearchParams(window.location.search);
      const protocol = urlParams["protocol"] || "https:";
      const host = urlParams["host"] || "app.storyblok.com";
      const targetOrigin = `${protocol}//${host}`;

      addEventListener("message", this.processMessage, false);

      window.parent.postMessage(
        {
          action: "plugin-changed",
          uid: this.uid,
          event: "showAssetModal",
          field: `breakpoints[${index}].rules[${ruleIndex}].value`
        },
        targetOrigin
      );
    },
    processMessage(e) {
      if (e.data.action !== "asset-selected") {
        return;
      }

      // Delete the target field if it was created by the default event handler (it's not needed)
      if (e.data.field in this.model) {
        delete this.model[e.data.field];
      }

      _set(this.model, e.data.field, e.data.filename);

      removeEventListener("message", this.processMessage, false);
    },
    initWith() {
      return {
        breakpoints: [
          {
            name: "xsmall",
            displayName: "Extra Small",
            minWidth: 0,
            rules: []
          },
          {
            name: "small",
            displayName: "Small",
            minWidth: 321,
            rules: []
          },
          {
            name: "medium",
            displayName: "Medium",
            minWidth: 641,
            rules: []
          },
          {
            name: "large",
            displayName: "Large",
            minWidth: 1025,
            rules: []
          },
          {
            name: "xlarge",
            displayName: "Extra Large",
            minWidth: 1441,
            rules: []
          }
        ],
        // This is the name of our plugin.
        plugin: "style-editor"
      };
    }
  }
};
</script>

<style>
.StyleEditor__day + .StyleEditor__day {
  margin-top: 10px;
}

.StyleEditor__day-name {
  margin-bottom: 5px;
}

.StyleEditor__list {
  padding-left: 0;
}

.StyleEditor__separator {
  margin-right: 4px;
  margin-left: 4px;
}

.StyleEditor__list-item + .StyleEditor__list-item {
  margin-top: 5px;
}
</style>

Thanks again for your help.