Using Watchers and Lifecycle Hooks

Pexels.com is a photo sharing site that allows users to curate collections of photos created by themselves and others.

Pexels.com also provides an API that can be used to download images to a web app.  The documentation for the API can be found here.

Pexels.com limits the number of requests that an application can make to 200 requests per hr and 20,000 requests per month.  They control access to their servers via API keys which can be obtained from their website.

Create a Pexels Account

Before we start coding, navigate to pexels.com and log in with a Google account.

Then navigate to https://www.pexels.com/api/ and click on the button labelled YourAPI key.  Fill out the form with the following data.

  • Project Name: College course project
  • Project Category: Personal User / Just for Fun
  • Explain briefly …: I’m learning asynchronous JavaScript programming in a college course and will use the API to download images to a demo web app.
  • URL of your website …: Your website domain name (e.g mine is salmon-rock-03c191a0f.2.azurestaticapps.net)

Click the checkbox to agree to the Term of Service then press the green button labelled Generate API Key.

You can copy the API key to your hard drive if you like, however you’ll be emailed a link to the key so that you can retrieve it at any time.

Create a Curated Collection

Think of a theme, then on pexels.com browse the photos that are available. When you find a photo that you want to add to a collection, select the photo. On the page that displays an individual photo you’ll find a button labelled Collect.  Press the collect button.  On the modal that appears, click on the button labeled Create new col..  Then in the subsequent modal provide a name for the collection and press Create New Collection.  You’ll then see a modal that lists your collections.  Click on the collection to which you wish to save the photo.  Add a half a dozen or so images to a collection.

Install the Pexels JavaScript Library

To use Pexels in a web app, install the pexels module in your project using the following command:

npm install pexels --save

Edit vite.config.js

While debugging my demo app, I found that my Vue component worked in development mode, but failed when pushed to my Azure site.  The problem was that Pexels uses CommonJS’ require statement to import another module.  CommonJS is a node.js construct for importing modules.  Vue doesn’t support require and, as you’ve seen, uses ES6’s more modern import statements instead.

We can have Vite automatically convert the require statements to import statements by adding the following to vite.config.js.

export default defineConfig({
  ...

  build: {
    commonjsOptions: {
      transformMixedEsModules: true
    }
  }
})

The Script

The JavaScript in this example utilizes the Composition API, therefore require the setup directive in the <script> element.

<script setup>

...

</script>

For security reasons, we shouldn’t write an API key in client side code since anyone can find the API key in the client side code and use it in another app.  If the API key was associated with a pay-as-you-go service, which our pexels.com keys are not, we would be responsible for all of the resources accessed using the API key.  In our case, we’ll go ahead and embed the pexels.com API keys in our client code since our apps are on the dark-web and we’re not publicizing them.

In your component’s <script setup> section, get the pexel’s createClient() method from the pexels module and call it passing it your API key, like as shown below.

import { createClient } from 'pexels'
...
 
const client = createClient('YOUR_API_KEY');

The client object that is returned by createClient() can be used to access the pexels.com API.  The client object can be used to do the following:

Next we need to create an object that will hold the meta-data for the photos in our curated collection and an object that will hold the information about a specific photo which we can use to create an <img> element on the page.

import { ref, reactive } from 'vue'
...

const collection = reactive({
  media: {},
})

const photo = reactive({
  name: "",
  photographer: "",
  photographer_url: "",
  url: ""
})
Get the Meta-data for Your Collection

We can get the meta-data for all of our personally curated collections by calling client.collections.all().  This method returns an object with a collections property containing an array of the meta-data for all of our collections.  The meta-data includes an id for the collection, the collection name, and the number of photos in the collection.

In the function below, after we obtain the meta-data for our collections, we grab the id of the first collection and call client.collections.media(), passing to it the id, to retrieve the meta-data for all of the photos in that collection.  The photo meta-data includes the photos title, name of photographer, url for the photgrapher’s collection, and a set of urls for the photo – each providing the photo of a particular size.  In the last line of the function we save the photo meta-data in our reactive object.

async function getCollection() {
  // get the meta-data for all collections
  let result = await client.collections.all({ per_page: 1 })

  // get the id of first collection
  const id = result.collections[0].id

  // get media for the collection
  result = await client.collections.media({ id, type: 'photos', per_page: 6 })

  // cache the media meta-data
  collection.media = result.media
}
Get the Data Needed to Create a <img> Element

We can now set the reactive photo object’s properties using the data in collection.media.   In the function below we assume there is a variable named index that holds a valid index for the collection.media object.

Note that the alt property holds the title of the photo, the photographer property holds the name of the photographer, the photographer_url property holds a url for the photographers collection on the pexel’s website, and the src property contains an array of urls for the photo.  src.tiny provides a url to the smallest sized version of the photo.

async function loadImage() {
  let index = ...

  photo.title = collection.media[index].alt
  photo.photographer = collection.media[index].photographer
  photo.photographer_url = collection.media[index].photographer_url
  photo.url = collection.media[index].src.tiny
}
Display the Photo

Since the photo object is reactive, we can bind photo.url to the src attribute in the <img> element, display the title and photographer’s name using mustache syntax, and bind the photo.photographer_url to the href attribute of a <a> element to provide a link the photographer’s collection on pexels.com.

<img :src="photo.url">
{{ photo.title }} <br>
<a :href="photo.photographer_url" target="_blank"> {{ photo.photographer }} </a>
Watching the Collection

Since we can’t call loadImage() until the collection data has been retrieved from the pexel.com API we need a way to watch the collection object.  Vue provides watchers which trigger a function’s execution when a reactive source is modified.

When using the Vue Composition API, we can set up a watcher by importing the watch() function and then call it. The first argument to watch() is any reactive source (e.g. a reactive or ref object) that we want to watch and the second argument is a callback function.

In the example below we register a lambda expression as the handler. In the body of the lambda expression we check to see if the length of the collection’s media property is greater than 0.  If so, we call loadImage().  By default the watcher is a deep watch, meaning that if any of the properties of the source are modified (no matter how deep), the hander will be invoked.

import { watch } from 'vue'
...

watch(collection, () => {
  console.log("collection changed")

  if (collection.media.length > 0) {
    loadImage()
  }
})

If we want to watch a specific property of a reactive object we can pass a getter as the first argument to watch().  The call to watch below is equivalent to the call above.

watch(() => { return collection.media },
  (newValue, oldValue) => {
    console.log("collection changed")

    if(newValue.length > 0) {
      loadImage()
    }
  }
)

We can also use Vue’s watchEffect() function to watch a reactive source.  watchEffect() does not take a reactive source as an argument but rather watches only reactive properties that are referenced in the callback function passed to it.  Below is an equivalent watcher that utilizes watchEffect.  Note that the function passed to watchEffect will be invoked immediately, and then also whenever any of the reactive properties in the function are modified.

import { watchEffect } from 'vue'
...

watchEffect(() => {
  console.log("collection changed")

  if (collection.media.length > 0) {
    loadImage()
  }
})
Timing Watchers

Let’s consider the timing of the watcher in our app.  We’ve programmed our app so that when the collection object changes, the loadImage() function is run.  loadImage() then sets the photo object’s properties.  Since these reactive properties are used in the template, when we change the values of the photo properties Vue updates the DOM.

There are some situations (though not in our running example) where we want to a watcher’s callback function to run after Vue has updated the DOM.  We can instruct Vue to run the callback after the DOM has been updated by passing an object (with the flush property set to 'post') as the third argument to watch() (as shown below) or by calling watchPostEffect().

watch(source, callback, {
  flush: 'post'
})
Launching getCollection()

As Vue creates and updates a component, it goes through a sequence of lifecycle stages. Before and after some of these stages, Vue emits custom events which we can register callback functions (aka hooks) that are called when the event is emitted.  This allows us to inject code at specific times during the component’s lifecycle.

If we want to call getCollection() after Vue has mounted the component in the DOM, can can call onMounted and pass it a handler that calls getCollection() as shown below.

import { onMounted } from 'vue'
...

onMounted(() => {
  console.log("onmounted")
  getCollection()
})
Now, after Vue has mounted the component in the DOM, getCollection() is invoked which starts the sequence of  events which, in the end, will set the attributes of the <img> element and display information about the photo’s photograher in the component.

 

© 2023, Eric.