cloudinary / cloudinary-vue

Cloudinary components library for Vue.js application, for image and video optimization.
https://cloudinary.com/documentation/vue_integration
97 stars 10 forks source link

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

Open alxistn opened 3 years ago

alxistn commented 3 years ago

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

patrick-tolosa commented 3 years ago

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?

alxistn commented 3 years ago

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 commented 3 years ago

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}`
eyalktCloudinary commented 3 years ago

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).

alxistn commented 3 years ago

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

.

patrick-tolosa commented 3 years ago

@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?

alxistn commented 3 years ago

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>
patrick-tolosa commented 3 years ago

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.

alxistn commented 3 years ago

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

patrick-tolosa commented 3 years ago

@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.

alxistn commented 3 years ago

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