cloudinary-vue icon indicating copy to clipboard operation
cloudinary-vue copied to clipboard

Add the possibility to specify css class to generated img html tag

Open alxistn opened this issue 4 years ago • 11 comments

Explain your use case

I want to be able to add a class to cld-image component that will be passed down to the generated element

Do you have a proposed solution?

Create a props to cld-image component named "imgClass" that will be concatenated to the rendered element

alxistn avatar Aug 12 '21 08:08 alxistn

Hey @alxistn

You should be able to pass a class prop directly to the component, this will be merged into the tag classes

    <cld-image
        cloudName="demo"
        class="my-class-name"
        public-id="woman"
    >

This results in this HTML: (Abbreviated the TransformationURL for clarity)

<img cloudname="demo" class="my-class-name cld-image cld-image-loaded" src={TransformationURL}>

Is that what you had in mind?

patrick-tolosa avatar Aug 12 '21 11:08 patrick-tolosa

Yes exactly !

Or to be more specific, because you could still want to style the container it could be something like that:

<cld-image
    cloudName="demo"
    class="high-level-class"
    imgClass="my-class-name-for-img"
    public-id="woman"
/>

and that will generate:

<div class="high-level-class">
    <img class="my-class-name-for-img" />
</div>

alxistn avatar Aug 12 '21 13:08 alxistn

Here is an example implementation that could be done:

<script>
import { setup } from '../../mixins/setup';
import { compute } from '../../mixins/compute';
import { register } from '../../mixins/registerTransformation'
import { computePlaceholder } from '../../helpers/computeOptions'
import { getCldPlaceholder, isCldPlaceholder } from '../../helpers/findComponent'
import {
  ACCESSIBILITY_TRANSFORMATIONS,
  PLACEHOLDER_TRANSFORMATIONS,
  COMPONENTS,
  LAZY_LOADING,
  IMAGE_CLASSES,
  IMAGE_WITH_PLACEHOLDER_CSS,
  RESPONSIVE_CSS,
  PLACEHOLDER_CLASS,
  CLD_IMAGE_WRAPPER_CLASS
} from '../../constants';
import { size } from "../../mixins/size";
import { lazy } from "../../mixins/lazy";
import {getDevicePixelRatio} from '../../utils/getDevicePixelRatio';

/**
 * Deliver images and specify image transformations using the cld-image (CldImage) component,
 * which automatically generates an `<img>` tag including the dynamic URL of the image source.
 *
 *
 * You can optionally include [cld-transformation](#cldtransformation) components to define transformations to apply to the delivered image.
 *
 * For more information see the
 * <a href="https://cloudinary.com/documentation/vue_image_manipulation#cldvideo_component" target="_blank">
 * cld-image component</a> and
 * <a href="https://cloudinary.com/documentation/image_transformations#embedding_images_in_web_pages"
 * target="_blank">embedding images in web pages</a> documentation.
 */
export default {
  name: COMPONENTS.CldImage,
  mixins: [setup, compute, lazy, size, register],
  props: {
    /**
     * The css class that will be added to the <img> tag
     */
    imgClass: {
      type: String,
      default: ""
    },
    /**
     * The unique identifier of an uploaded image.
     */
    publicId: { type: String, default: "", required: true },
    /**
     * Whether to generate a JPEG using the [progressive (interlaced) JPEG
     * format](https://cloudinary.com/documentation/transformation_flags#delivery_and_image_format_flags).
     */
    progressive: {
      type: Boolean,
      default: false
    },
    /**
     * **Deprecated**
     *
     * The placeholder image to use while the image is loading. Possible values:
     * - `"blur"` to use blur placeholder
     * - `"lqip"` to use a low quality image
     * - `"color"` to use an average color image
     * - `"pixelate"` to use a pixelated image
     * - `"vectorize"` to use a vectorized image
     * - `"predominant-color" to use a predominant color image
     * @deprecated - Use CldPlaceholder instead
     */
    placeholder: {
      type: String,
      default: "",
      validator: value => !value || !!PLACEHOLDER_TRANSFORMATIONS[value]
    },

    /**
     * Out-of-the-box support for accessibility mode, including colorblind and dark/light mode
     */
    accessibility: {
      type: String,
      default: "",
      validator: value => !value || !!ACCESSIBILITY_TRANSFORMATIONS[value]
    }
  },
  data() {
    return {
      imageLoaded: false,
      cloudinary: null,
    }
  },
  methods: {
    load() {
      this.imageLoaded = true;
    },
    renderImageOnly(src, hasPlaceholder = false) {
      const imgClass = `${IMAGE_CLASSES.DEFAULT} ${!this.imageLoaded ? IMAGE_CLASSES.LOADING : IMAGE_CLASSES.LOADED} ${this.imgClass}`
      const style = {
        ...(this.responsive ? RESPONSIVE_CSS[this.responsive] : {}),
        ...(!this.imageLoaded && hasPlaceholder ? IMAGE_WITH_PLACEHOLDER_CSS[IMAGE_CLASSES.LOADING] : {})
      }

      return (
        <img
          attrs={this.$attrs}
          src={src}
          loading={this.hasLazyLoading ? LAZY_LOADING : null}
          class={imgClass}
          onload={this.load}
          style={style}
        />
      )
    },
    renderComp(children) {
      this.setup(this.$attrs)

      if (this.placeholder) {
        // eslint-disable-next-line
        console.warn ('The prop "placeholder" has been deprecated, please use the cld-placeholder component');
      }

      const responsiveModeNoSize = this.responsive && !this.size
      const lazyModeInvisible = this.hasLazyLoading && !this.visible
      const options = this.computeURLOptions()

      let src = responsiveModeNoSize || lazyModeInvisible ? '' : this.cloudinary.url(this.publicId, this.toSnakeCase(options));
      // Update dpr_auto to dpr_1.0, 2.0 etc but only for responsive mode
      // This matches the behaviour in other SDKs
      if (this.responsive) {
        src = src.replace(/\bdpr_(1\.0|auto)\b/g, 'dpr_' + getDevicePixelRatio(true));
      }

      const cldPlaceholder = getCldPlaceholder(children)
      const cldPlaceholderType = cldPlaceholder ? (cldPlaceholder.componentOptions?.propsData?.type || 'blur') : ''
      const placeholderType = cldPlaceholderType || this.placeholder

      const placeholderOptions = placeholderType ? this.toSnakeCase(computePlaceholder(placeholderType, options)) : null

      if (!placeholderOptions) {
        return this.renderImageOnly(src)
      }

      const placeholder = responsiveModeNoSize ? '' : this.cloudinary.url(this.publicId, placeholderOptions)
      const displayPlaceholder = !this.imageLoaded && placeholder

      return (
        <div class={CLD_IMAGE_WRAPPER_CLASS}>
        { this.renderImageOnly(src, true) }
        { displayPlaceholder && (
          <img
              src={placeholder}
              attrs={this.$attrs}
              class={PLACEHOLDER_CLASS}
              style={IMAGE_WITH_PLACEHOLDER_CSS[PLACEHOLDER_CLASS]}
            />) }
        </div>
      )
    }
  },
  render(h) {
    if (!this.publicId) return null

    const children = this.$slots.default || []
    const hasExtraTransformations = children.length > 1 || (children.length === 1 && !isCldPlaceholder(children[0]))

    /* Render the children first to get the extra transformations (if there is any) */
    if (hasExtraTransformations && !this.extraTransformations.length) {
      return h(
        "img", {
          attrs: this.attrs
        },
        this.$slots.default
      );
    }

    return this.renderComp(children)
  }
};
</script>

Here there is a new prop named "imgClass" of type String and default to an empty string.

And then when creating the and its class you concatenate the "imgClass" prop with the existing cloudinary classes like this:

const imgClass = `${IMAGE_CLASSES.DEFAULT} ${!this.imageLoaded ? IMAGE_CLASSES.LOADING : IMAGE_CLASSES.LOADED} ${this.imgClass}`

alxistn avatar Aug 12 '21 13:08 alxistn

Hey @alxistn, the cld-image component returns an <img> element (without a wrapping <div>). If you want to wrap it you can place it inside a div -

<div class="high-level-class">
    <cld-image
      cloudName="demo"
      class="my-class-name-for-img"
      public-id="woman"
      />
</div>

which would resolve to -

<div class="high-level-class">
    <img cloudname="demo" 
         class="my-class-name-for-img cld-image cld-image-loaded" 
         src={TransformationURL}>
</div>

This looks like what you are looking for - https://github.com/cloudinary/cloudinary-vue/issues/145#issuecomment-897651164

Another thing to note is that if you do wrap the cld-image component with a div element that has a class (like the above example), you could just reference the image in CSS with the .high-level-class img {...} selector (in that case you do not need to specify a class in the cld-image component).

eyalktCloudinary avatar Aug 12 '21 15:08 eyalktCloudinary

Well try it yourself you will see it does render the tag inside a

.

alxistn avatar Aug 12 '21 15:08 alxistn

@alxistn it depends on how you use the component, for example a simple rendering as I've shown above:

    <cld-image
        cloudName="demo"
        class="my-class-name"
        public-id="woman"
    >

Generates this (full) html - without a div

<img cloudname="demo" class="my-class-name cld-image cld-image-loaded" src={TransformationURL}>

However, if you're using a placeholder an example, you are indeed getting a wrapping div, with this HTML

<div class="my-class-name cld-image-wrapper" cloudname="demo">
<img cloudname="demo" src="{TransformationURL}" class="cld-image cld-image-loaded" style="">
</div>

Note how in this case, only the top-most HTML element receives the className (So just the div, but not the img)

Given this information, can you explain what exactly you'd like to achieve?

patrick-tolosa avatar Aug 12 '21 15:08 patrick-tolosa

Given this I would need this result:

<div class="my-class-name cld-image-wrapper" cloudname="demo">
<img cloudname="demo" src="{TransformationURL}" class="cld-image cld-image-loaded my-class-name" style="">
</div>

alxistn avatar Aug 12 '21 16:08 alxistn

Thanks @alxistn , now it's understood.

Although this goes against the way Vue works (The top-most component always gets the class passed to the component), this does sound like something that can help make things more consistent.

It looks like the best approach would be to add a new prop specifically for the img tag. Our team is currently focused on our new generation of the SDKs (including the major Vue release), however, if you can open a PR for this issue, we'll be happy to review and assist in merging it.

patrick-tolosa avatar Aug 15 '21 13:08 patrick-tolosa

Great ! This is what I had in mind but I'm not sure how to process given that I don't have the rights to do so

alxistn avatar Aug 16 '21 06:08 alxistn

@alxistn You should be able to fork the project (Top right in your github UI), once you've forked the repo, you can commit to your fork copy your changes, then you can open a PR from the fork to this repository.

Once the PR is open we'll get a notification and we can start the review process.

patrick-tolosa avatar Aug 25 '21 12:08 patrick-tolosa

@patrick-tolosa thanks for the explanation. I was able to submit the PR 👌 (https://github.com/cloudinary/cloudinary-vue/pull/148)

alxistn avatar Aug 27 '21 08:08 alxistn