Build a social sharing component in Vue3
Published: 2022-04-01 13:44:19

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`](https://www.npmjs.com/package/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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lf3y6e0l5mawo2k3ourj.png)

well, this is easy to write to Vue3, and we can also find a example [here](https://github.com/nicolasbeauvais/vue-social-sharing/blob/master/examples/index.js): 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bazv5zoo8glaebnniqta.png)
 

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3c9ggwcvsf8ft11fpden.png)

we can keep that, no need to change anything

now the remaining issue is `share-network.js`: 

```javascript 
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)
    }
  }
}
```

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fg6h2ixylz5qqnyn79af.png)

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: 

- [Render Function in Vue3](https://vuejs.org/api/render-function.html#render-function-apis)

- [Render Function in Vue2](https://v2.vuejs.org/v2/guide/render-function.html#createElement-Arguments)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g7wqu90u4c7zwlmos9j7.png)

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: 

```javascript
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
			);
	}
};

```

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: 

```json
{
    "a",
    {
        "class": "share-network-facebook",
        "on": {},
        "attrs": {
            "href": "javascript:void(0)"
        }
    }
}
```

the `a` tag is 

and you can export that and register your component to use

example: 

```javascript 

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

`quote` is for twitter

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

```html
<a href="javascript:void(0)" data-v-5be083fa="">
// whatever your content here 
</a>
```

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