DEV Community

Jingtian Zhang
Jingtian Zhang

Posted on • Updated on

Build a social sharing component in Vue3

We are trying to migrate the entire project to Vue3, so there were lots of rewrite, including basic components, from button to message, dialog, etc, and all the business logic and related components.

One component we need to rewrite was vue-social-sharing, which is really easy to use in Vue2.

Dissecting the source code

when we dig into what inside source code of vue-social-sharing, we can find three source files :

  • vue-social-sharing.js
  • network.js
  • share-network.js

well, the first one vue-social-sharing.js is the entry file where it instructs how to mount the component in the correct way:

Image description

well, this is easy to write to Vue3, and we can also find a example here:

Image description

just a simple Vue.use(), easy to understand, lets check other files

in network.js, it just exports bunch of social media links:

Image description

we can keep that, no need to change anything

now the remaining issue is share-network.js:

import AvailableNetworks from './networks'

let $window = typeof window !== 'undefined' ? window : null

export function mockWindow (self) {
  $window = self || window // mock window for unit testing
}

export default {
  name: 'ShareNetwork',

  props: {
    /**
     * Name of the network to display.
     */
    network: {
      type: String,
      required: true
    },

    /**
     * URL of the content to share.
     */
    url: {
      type: String,
      required: true
    },

    /**
     * Title of the content to share.
     */
    title: {
      type: String,
      required: true
    },

    /**
     * Description of the content to share.
     */
    description: {
      type: String,
      default: ''
    },

    /**
     * Quote content, used for Facebook.
     */
    quote: {
      type: String,
      default: ''
    },

    /**
     * Hashtags, used for Twitter and Facebook.
     */
    hashtags: {
      type: String,
      default: ''
    },

    /**
     * Twitter user, used for Twitter
     * @var string
     */
    twitterUser: {
      type: String,
      default: ''
    },

    /**
     * Media to share, used for Pinterest
     */
    media: {
      type: String,
      default: ''
    },

    /**
     * HTML tag used by the Network component.
     */
    tag: {
      type: String,
      default: 'a'
    },

    /**
     * Properties to configure the popup window.
     */
    popup: {
      type: Object,
      default: () => ({
        width: 626,
        height: 436
      })
    }
  },

  data () {
    return {
      popupTop: 0,
      popupLeft: 0,
      popupWindow: undefined,
      popupInterval: null
    }
  },

  computed: {
    /**
     * List of available networks
     */
    networks () {
      return this.$SocialSharing
        ? this.$SocialSharing.options.networks
        : AvailableNetworks
    },

    /**
     * Formatted network name.
     */
    key () {
      return this.network.toLowerCase()
    },

    /**
     * Network sharing raw sharing link.
     */
    rawLink () {
      const ua = navigator.userAgent.toLowerCase()

      /**
       * On IOS, SMS sharing link need a special formatting
       * Source: https://weblog.west-wind.com/posts/2013/Oct/09/Prefilling-an-SMS-on-Mobile-Devices-with-the-sms-Uri-Scheme#Body-only
       */
      if (this.key === 'sms' && (ua.indexOf('iphone') > -1 || ua.indexOf('ipad') > -1)) {
        return this.networks[this.key].replace(':?', ':&')
      }

      return this.networks[this.key]
    },

    /**
     * Create the url for sharing.
     */
    shareLink () {
      let link = this.rawLink

      /**
       * Twitter sharing shouldn't include empty parameter
       * Source: https://github.com/nicolasbeauvais/vue-social-sharing/issues/143
       */
      if (this.key === 'twitter') {
        if (!this.hashtags.length) link = link.replace('&hashtags=@h', '')
        if (!this.twitterUser.length) link = link.replace('@tu', '')
      }

      return link
        .replace(/@tu/g, '&via=' + encodeURIComponent(this.twitterUser))
        .replace(/@u/g, encodeURIComponent(this.url))
        .replace(/@t/g, encodeURIComponent(this.title))
        .replace(/@d/g, encodeURIComponent(this.description))
        .replace(/@q/g, encodeURIComponent(this.quote))
        .replace(/@h/g, this.encodedHashtags)
        .replace(/@m/g, encodeURIComponent(this.media))
    },

    /**
     * Encoded hashtags for the current social network.
     */
    encodedHashtags () {
      if (this.key === 'facebook' && this.hashtags.length) {
        return '%23' + this.hashtags.split(',')[0]
      }

      return this.hashtags
    }
  },

  render: function (createElement) {
    if (!this.networks.hasOwnProperty(this.key)) {
      throw new Error('Network ' + this.key + ' does not exist')
    }

    const node = {
      class: 'share-network-' + this.key,
      on: {
        click: () => this[this.rawLink.substring(0, 4) === 'http' ? 'share' : 'touch']()
      }
    }

    if (this.tag === 'a') node.attrs = { href: 'javascript:void(0)' }

    return createElement(this.tag, node, this.$slots.default)
  },

  methods: {
    /**
     * Center the popup on multi-screens
     * http://stackoverflow.com/questions/4068373/center-a-popup-window-on-screen/32261263
     */
    resizePopup () {
      const width = $window.innerWidth || (document.documentElement.clientWidth || $window.screenX)
      const height = $window.innerHeight || (document.documentElement.clientHeight || $window.screenY)
      const systemZoom = width / $window.screen.availWidth

      this.popupLeft = (width - this.popup.width) / 2 / systemZoom + ($window.screenLeft !== undefined ? $window.screenLeft : $window.screenX)
      this.popupTop = (height - this.popup.height) / 2 / systemZoom + ($window.screenTop !== undefined ? $window.screenTop : $window.screenY)
    },

    /**
     * Shares URL in specified network.
     */
    share () {
      this.resizePopup()

      // If a popup window already exist, we close it and trigger a change event.
      if (this.popupWindow && this.popupInterval) {
        clearInterval(this.popupInterval)

        // Force close (for Facebook)
        this.popupWindow.close()

        this.emit('change')
      }

      this.popupWindow = $window.open(
        this.shareLink,
        'sharer-' + this.key,
        ',height=' + this.popup.height +
        ',width=' + this.popup.width +
        ',left=' + this.popupLeft +
        ',top=' + this.popupTop +
        ',screenX=' + this.popupLeft +
        ',screenY=' + this.popupTop
      )

      // If popup are prevented (AdBlocker, Mobile App context..), popup.window stays undefined and we can't display it
      if (!this.popupWindow) return

      this.popupWindow.focus()

      // Create an interval to detect popup closing event
      this.popupInterval = setInterval(() => {
        if (!this.popupWindow || this.popupWindow.closed) {
          clearInterval(this.popupInterval)

          this.popupWindow = null

          this.emit('close')
        }
      }, 500)

      this.emit('open')
    },

    /**
     * Touches network and emits click event.
     */
    touch () {
      window.open(this.shareLink, '_blank')

      this.emit('open')
    },

    emit (name) {
      this.$root.$emit('share_network_' + name, this.key, this.url)
      this.$emit(name, this.key, this.url)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

when we are wondering what is inside this, lets just print it out

Image description

here it shows it is a VueComponent instance, we can rewrite methods, computed property and most of others to Vue3 style first, and in the end, let's take a look of the render function

Rewrite

methods

  • to function or arrow functions

variables

  • use ref or reactive to define

props

  • use defineProps to define props
  • to destructure props, use toRefs or toRef

emits

  • use defineEmits to define emit

computed

  • use computed(() => xxx) to define computed properties

watch

  • use watch(() => props.xxx, (newVal, oldVal) => xxxxxx) to watch for certain property
  • watchEffect is another thing we can use

Render Function

First, let's read through render function topic in both Vue2 and Vue3 doc:

here we can see that in Vue2 the render function has a default template:

Image description

in Vue2:

  • createElement is actually a function, can print it out and take a look
  • render function eventually returns createElement
  • createElement can pass 3 args
    • 1st one required, String/Object/Function
    • 2nd optional, Object
    • 3rd optional, String/Array

in Vue3, there are major changes, actually, I think the doc explains very clearly about changes, so I will just proceed to the following part.

Rewrite Render Logic in Vue3

now let's write our share-network.js here:

import AvailableNetworks from './network';
import {
    ref,
    computed,
    h,
    onMounted,
    reactive,
    toRefs,
    getCurrentInstance
} from 'vue';
let $window = typeof window !== 'undefined' ? window : null;

export default {
    name: 'DPSharing',
    props: {
        network: {
            type: String,
            required: true
        },
        url: {
            type: String,
            required: true
        },
        title: {
            type: String,
            required: true
        },
        description: {
            type: String,
            default: ''
        },
        quote: {
            type: String,
            default: ''
        },
        hashtags: {
            type: String,
            default: ''
        },
        twitterUser: {
            type: String,
            default: ''
        },
        media: {
            type: String,
            default: ''
        },
        tag: {
            type: String,
            default: 'a'
        },
        popup: {
            type: Object,
            default: () => ({
                width: 626,
                height: 436
            })
        }
    },
    emits: ['change', 'close', 'open'],
    setup(props, context) {
        const { slots, emit } = context;
        const popupTop = ref(0);
        const popupLeft = ref(0);
        const popupWindow = ref(undefined);
        const popupInterval = ref(null);

        const {
            network,
            url,
            title,
            description,
            quote,
            hashtags,
            twitterUser,
            media,
            tag,
            popup
        } = toRefs(props);

        const networks = computed(() => {
            return AvailableNetworks;
        });

        const key = computed(() => {
            return network.value.toLowerCase();
        });

        const rawLink = computed(() => {
            const ua = navigator.userAgent.toLowerCase();
            if (
                key.value === 'sms' &&
                (ua.indexOf('iphone') > -1 || ua.indexOf('ipad') > -1)
            ) {
                return networks.value[key.value].replace(':?', ':&');
            }
            return networks.value[key.value];
        });

        const shareLink = computed(() => {
            console.log('check sharelink');
            let link = rawLink.value;

            if (key.value === 'twitter') {
                if (!hashtags.value.length) link = link.replace('&hashtags=@h', '');
                if (!twitterUser.value.length) link = link.replace('@tu', '');
            }

            return link
                .replace(/@tu/g, '&via=' + encodeURIComponent(twitterUser.value))
                .replace(/@u/g, encodeURIComponent(url.value))
                .replace(/@t/g, encodeURIComponent(title.value))
                .replace(/@d/g, encodeURIComponent(description.value))
                .replace(/@q/g, encodeURIComponent(quote.value))
                .replace(/@h/g, encodedHashtags.value)
                .replace(/@m/g, encodeURIComponent(media));
        });

        const encodedHashtags = computed(() => {
            console.log('check encodedHashtags');
            if (key === 'facebook' && hashtags.value.length) {
                return '%23' + hashtags.value.split(',')[0];
            }

            return hashtags.value;
        });

        const resizePopup = () => {
            console.log('resize popup method triggered here ');

            const width =
                $window.innerWidth ||
                document.documentElement.clientWidth ||
                $window.screenX;
            const height =
                $window.innerHeight ||
                document.documentElement.clientHeight ||
                $window.screenY;
            const systemZoom = width / $window.screen.availWidth;

            popupLeft.value =
                (width - popup.width) / 2 / systemZoom +
                ($window.screenLeft !== undefined
                    ? $window.screenLeft
                    : $window.screenX);
            popupTop.value =
                (height - popup.height) / 2 / systemZoom +
                ($window.screenTop !== undefined ? $window.screenTop : $window.screenY);
        };

        const share = () => {
            // debugger;
            resizePopup();

            // If a popup window already exist, we close it and trigger a change event.
            if (popupWindow.value) {
                clearInterval(popupInterval.value);

                // Force close (for Facebook)
                popupWindow.value.close();
                emit('change');
            }

            popupWindow.value = $window.open(
                shareLink.value,
                'sharer-' + key.value,
                ',height=' +
                    popup.value.height +
                    ',width=' +
                    popup.value.width +
                    ',left=' +
                    popupLeft.value +
                    ',top=' +
                    popupTop.value +
                    ',screenX=' +
                    popupLeft.value +
                    ',screenY=' +
                    popupTop.value
            );

            // If popup are prevented (AdBlocker, Mobile App context..), popup.window stays undefined and we can't display it
            if (!popupWindow.value) return;

            popupWindow.value.focus();

            // Create an interval to detect popup closing event
            popupInterval.value = setInterval(() => {
                if (!popupWindow.value || popupWindow.value.closed) {
                    clearInterval(popupInterval.value);

                    popupWindow.value = null;

                    emit('close');
                }
            }, 500);

            emit('open');
        };

        const touch = () => {
            console.log('touch method triggered');
            window.open(shareLink.value, '_blank');
            emit('open');
        };

        const renderData = () => {
            if (!networks.value.hasOwnProperty(key.value)) {
                throw new Error('Network ' + key.value + ' does not exist');
            }

            const node = {
                class: 'share-network-' + key.value,
                on: {
                    click: rawLink.value.substring(0, 4) === 'http' ? share : touch
                },
                attrs: {
                    href:
                        rawLink.value.substring(0, 4) === 'http'
                            ? shareLink.value
                            : rawLink.value
                }
            };

            if (tag.value === 'a') {
                node.attrs = { href: 'javascript:void(0)' };
            }

            return [tag.value, node, slots.default()];
        };

        const data = renderData();
        const tg = `${data[0]}`;
        const node = data[1];
        const content = data[2];

        return () =>
            h(
                tg,
                {
                    onClick: node.on.click,
                    href: node.attrs.href
                },
                content
            );
    }
};

Enter fullscreen mode Exit fullscreen mode

pay attention to the render function we return in the end, we first identify a function called renderData, which returns data for rendering purpose, and console.log() it to show the response:

{
    "a",
    {
        "class": "share-network-facebook",
        "on": {},
        "attrs": {
            "href": "javascript:void(0)"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

the a tag is

and you can export that and register your component to use

example:


<COSharing network="facebook" :title="'facebook'" url="https://www.google.com" quote="hello" >
 click me to open whatsapp sharing
</COSharing>
Enter fullscreen mode Exit fullscreen mode

quote is for twitter

and when you inspect your element, you will find element wrapped in a tag:

<a href="javascript:void(0)" data-v-5be083fa="">
// whatever your content here 
</a>
Enter fullscreen mode Exit fullscreen mode

and now we have a successful rewrite into Vue3 for vue-social-sharing library, do not need to rely on the update of that package anymore, you can learn to create your own, which is actually a good practice

Top comments (0)