Nullable Object in Vue 3 Composition API

I am utilizing the Vue 3 Composition api along with Typescript to create pinch zoom functionality using the HammerJS package.

In my Vue application, I am attempting to replicate a functional example implemented in JavaScript from CodePen: https://codepen.io/bakho/details/GBzvbB.

However, I have encountered some challenges while trying to integrate it into my Vue project and I'm unsure how to address them.

The following errors have been identified:

// Object is possibly 'null'.
imageContainer.value.offsetWidth;

// Object is possibly 'null'.
imageContainer.value.appendChild(displayImage);

// Object is possibly 'null'.
imageContainer.value.addEventListener...

Below is the complete source code:

<template>
  <h1>Image Zoom</h1>
  <div class="imageContainer" ref="imageContainer"></div>
</template>

<script lang="ts">
import Hammer from "hammerjs";
import { defineComponent } from "vue";
import { ref } from 'vue';

export default defineComponent({
  setup() {
    const imageUrl = "https://source.unsplash.com/random";
    const imageContainer = ref(null)

    let minScale = 1; let maxScale = 4; let imageWidth : any; let imageHeight : any; let containerWidth : any;
    let containerHeight : any; let displayImageX = 0; let displayImageY = 0; let displayImageScale = 1;
    let displayDefaultWidth : any; let displayDefaultHeight 
    let rangeX = 0; let rangeMaxX = 0; let rangeMinX = 0;
    let rangeY = 0; let rangeMaxY = 0; let rangeMinY = 0;

    // let displayImageRangeY = 0;

    let displayImageCurrentX = 0;
    let displayImageCurrentY = 0;
    let displayImageCurrentScale = 1;

    function resizeContainer() {
      containerWidth = imageContainer.value.offsetWidth;
      containerHeight = imageContainer.value.offsetHeight;
      if (displayDefaultWidth !== undefined && displayDefaultHeight !== undefined) {
        displayDefaultWidth = displayImage.offsetWidth;
        displayDefaultHeight = displayImage.offsetHeight;
        updateRange();
        displayImageCurrentX = clamp(displayImageX, rangeMinX, rangeMaxX);
        displayImageCurrentY = clamp(displayImageY, rangeMinY, rangeMaxY);
        updateDisplayImage(
          displayImageCurrentX,
          displayImageCurrentY,
          displayImageCurrentScale
        );
      }
    }
    resizeContainer();

    function clamp(value, min, max) {
      return Math.min(Math.max(min, value), max);
    }

    function clampScale(newScale) {
      return clamp(newScale, minScale, maxScale);
    }

    const displayImage = new Image();
        displayImage.src = imageUrl;
        displayImage.onload = function(){
        imageWidth = displayImage.width;
        imageHeight = displayImage.height;
        imageContainer.value.appendChild(displayImage);
        displayImage.addEventListener('mousedown', e => e.preventDefault(), false);
        displayDefaultWidth = displayImage.offsetWidth;
        displayDefaultHeight = displayImage.offsetHeight;
        rangeX = Math.max(0, displayDefaultWidth - containerWidth);
        rangeY = Math.max(0, displayDefaultHeight - containerHeight);
    }

    imageContainer.value.addEventListener('wheel', e => {
        displayImageScale = displayImageCurrentScale = clampScale(displayImageScale + (e.wheelDelta / 800));
        updateRange();
        displayImageCurrentX = clamp(displayImageCurrentX, rangeMinX, rangeMaxX)
        displayImageCurrentY = clamp(displayImageCurrentY, rangeMinY, rangeMaxY)
        updateDisplayImage(displayImageCurrentX, displayImageCurrentY, displayImageScale);  
    }, false);

    function updateDisplayImage(x, y, scale) {
        const transform = 'translateX(' + x + 'px) translateY(' + y + 'px) translateZ(0px) scale(' + scale + ',' + scale + ')';
        displayImage.style.transform = transform;
        displayImage.style.webkitTransform = transform;
        displayImage.style.transform = transform;
    }

    function updateRange() {
        rangeX = Math.max(0, Math.round(displayDefaultWidth * displayImageCurrentScale) - containerWidth);
        rangeY = Math.max(0, Math.round(displayDefaultHeight * displayImageCurrentScale) - containerHeight);
        
        rangeMaxX = Math.round(rangeX / 2);
        rangeMinX = 0 - rangeMaxX;

        rangeMaxY = Math.round(rangeY / 2);
        rangeMinY = 0 - rangeMaxY;
    }

    const hammertime = new Hammer(imageContainer);
    hammertime.get('pinch').set({ enable: true });
    hammertime.get('pan').set({ direction: Hammer.DIRECTION_ALL });

    hammertime.on('pan', ev => {  
        displayImageCurrentX = clamp(displayImageX + ev.deltaX, rangeMinX, rangeMaxX);
        displayImageCurrentY = clamp(displayImageY + ev.deltaY, rangeMinY, rangeMaxY);
        updateDisplayImage(displayImageCurrentX, displayImageCurrentY, displayImageScale);
    });

    hammertime.on('pinch pinchmove', ev => {
        displayImageCurrentScale = clampScale(ev.scale * displayImageScale);
        updateRange();
        displayImageCurrentX = clamp(displayImageX + ev.deltaX, rangeMinX, rangeMaxX);
        displayImageCurrentY = clamp(displayImageY + ev.deltaY, rangeMinY, rangeMaxY);
        updateDisplayImage(displayImageCurrentX, displayImageCurrentY, displayImageCurrentScale);
    });

    hammertime.on('panend pancancel pinchend pinchcancel', () => {
        displayImageScale = displayImageCurrentScale;
        displayImageX = displayImageCurrentX;
        displayImageY = displayImageCurrentY;
    }); 

    return {};
  },
});
</script>

<style>
.imageContainer {
  width: 96%;
  height: 96%;
  max-width: 800px;
  max-height: 600px;
  position: absolute;
  overflow: hidden;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  margin: auto;
  background: #2b2b2c;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.imageContainer > img {
  display: block;
  max-width: 100%;
  max-height: 100%;
  cursor: move;
  touch-action: none;
}
</style>

Could someone help troubleshoot what is causing this issue of Object is possibly 'null'?

Answer №1

These are the causes for the error:

  • const imageContainer = ref(null) -> the value is set to null, causing TypeScript to warn about potential errors when accessing properties of an object with an initial value of null. Additionally, using ref in a template means the element may or may not exist.

  • const imageContainer = document.querySelector('.imageContainer')
    -> querying for an HTML element that may not exist results in a potentially null value. TypeScript warns about accessing properties of this object that can lead to errors.

Solution:

  • You need to return the imageContainer variable from the setup function so that Vue binds it with ref in the template. Accessing the variable should be done within the onMounted function as the ref is not yet mounted in the DOM. Keep in mind that setup is called before the created and mounted hooks in the Vue.js component lifecycle, meaning there is no access to DOM elements at that point.
import { ref, onMounted } from 'vue'

export default {
  setup() {
    const imageContainer = ref(null)

    onMounted(() => {
      // access imageContainer variable here
    })

    return {
      imageContainer 
    }
  }
}

Answer №2

You may have encountered a Typescript error related to handling null variables. To resolve this issue, it is essential to define the type properly and address the possibility of the variable being null. Check out the guide on Typing ref in Vue documentation for more insights.

Below are some examples tailored to assist you:

// Specify the ref as HTMLElement or null
const imageContainer = ref<HTMLElement | null>(null);

...

// Determine width/height using imageContainer, fallback to 0 if null.
containerWidth = imageContainer.value?.offsetWidth ?? 0;
containerHeight = imageContainer.value?.offsetHeight ?? 0;

...

// Execute appendChild/addEventListener only if the ref is not null.
imageContainer.value?.appendChild(displayImage);

By implementing this strategy wherever the error occurs, you can effectively tackle the problem at hand.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

I am having difficulty retrieving information from the Laravel API

I am struggling to retrieve data from my Laravel application and display it in Vue CLI. While I can see the response, I am encountering difficulties when trying to show it in the Vue application. https://i.stack.imgur.com/tCgrd.png Whenever I attempt to f ...

Signaling an Event from a module in the node_modules directory to the Vue application

Is there a way to capture an event emitted by a node module and receive it in a Vue file? Sample code from the JavaScript node module: const EventEmitter = require('events') class Find extends EventEmitter { // code snippets here } class Fin ...

While attempting to update the package.json file, I encountered an error related to the polyfills in Angular

I have been working on a project with ng2 and webpack, everything was running smoothly until I updated the package.json file. Since then, I have been encountering some errors. Can anyone please assist me in identifying the issue? Thank you for any help! P ...

Unable to locate youtube.ts file in the Angular 2 project for the YoutubeAPI integration

I've been using a youtube.d.ts file from the DefinitelyTyped project. It functions perfectly in my WebStorm while I'm editing, but once I try to run it, I encounter a 404 error stating that typings/youtube.js is not found. This file doesn't ...

What is the most effective way to split time into two separate parts?

Suppose a user enters the time as 12:34 and we need to split it into two different parts to save it in an array like [12, 34]. How can this be achieved using Angular? I attempted to split them but my solutions were unsuccessful! I am currently utilizing & ...

Unable to set up enzyme adapter

Currently, I am in the process of setting up the enzyme adapter for testing purposes. The code snippet that I have is quite straightforward: import * as enzyme from 'enzyme'; import * as Adapter from 'enzyme-adapter-react-16'; enzyme. ...

Is there an issue with loading Vue list rendering due to Axios not returning the data?

Utilize the axios request api interface to fetch data and populate a list, but encounter a problem when trying to iterate through the properties of an object using v-for. Take a look at the javascript snippet below: var vm = new Vue({ el: '# ...

Using Redux and Typescript to manage user authentication states can help streamline the process of checking whether a user is logged in or out without the need for repetitive checks in the mapStateToProps function

In the process of developing a web application utilizing React & Redux, I am faced with defining two primary "states" - Logged In and Logged Out. To tackle this challenge, I have structured my approach incorporating a union type State = LoggedIn | LoggedO ...

Angular 7 error: No provider found for PagerService causing NullInjectorError

I have been struggling to get pagination working properly in my project. Below is the code I have written: pager.service.ts: import * as _ from 'underscore'; @Injectable({ providedIn: 'root', }) export class PagerService { ...

Different ways to update the AutoComplete Style attribute

MuiInput-formControl { margin-top: 16px; Is there a way to reset the marginTop property to zero? I attempted the following method, but it was not successful. MuiFormControlLabel: { marginTop: 0, }, <Autocomplete disabl ...

`I have another inquiry regarding static assets in Vue.js`

In my experience with vuejs, I have noticed a strange behavior when attempting to set the img src in a vue component. The image src keeps getting set to something resembling my publicPath (app/mm/dist/dev). Although this project utilizes vuetify, it is puz ...

Two unnamed objects cannot be combined using the AsyncPipe

Currently, I am looking to implement an autocomplete feature using Angular Material in Angular 8. Below is a snippet of the code used in the TypeScript file: @Input() admins: User[]; userGroupOptions: Observable<User[]>; filterFormFG: FormGrou ...

Techniques for adding data to an array using TypeScript

I am working on a project that involves creating a dynamic menu bar by fetching data from two different collections (supcat and cat) in order to combine them into a new array. However, I am encountering an issue with the push() method not working as expe ...

How to pass props to customize styles in MUI5 using TypeScript

Currently, I'm in the process of migrating my MUI4 code to MUI5. In my MUI4 implementation, I have: import { createStyles, makeStyles } from '@material-ui/core'; import { Theme } from '@material-ui/core/styles/createMuiTheme'; ty ...

Exploring the world of Vue and Pinia: managing and accessing data like

While delving into Vue and Pinia, I encountered a data management issue on the user side. On my main page, I showcase categories and auction items. However, upon navigating to a specific category in the catalog, the data for auction items remains in the st ...

Integrate a service component into another service component by utilizing module exports

After diving into the nestjs docs and exploring hierarchical injection, I found myself struggling to properly implement it within my project. Currently, I have two crucial modules at play. AuthModule is responsible for importing the UserModule, which conta ...

Tips for Invoking an Overloaded Function within a Generic Environment

Imagine having two interfaces that share some fields and another interface that serves as a superclass: interface IFirst { common: "A" | "B"; private_0: string; } interface ISecond { common: "C" | "D"; private_1: string; } interface ICommo ...

Navigating from the Login Page to the Dashboard in Vue.js following successful token validation

I am facing an issue with the code that is supposed to redirect the User to the dashboard page if they have a token. Despite generating a JWT token from my Spring Boot backend and sending it to Vue for processing, the redirection is not working as expect ...

Basic cordova application that transfers data from one page to another using typescript

Currently, I am developing an Apache Cordova application using TypeScript. However, I am facing a challenge in passing information from one HTML page to another using TypeScript. I would appreciate it if someone could guide me on the steps needed for nav ...

Unable to perform module augmentation in TypeScript

Following the guidelines provided, I successfully added proper typings to my react-i18next setup. The instructions can be found at: However, upon creating the react-i18next.d.ts file, I encountered errors concerning unexported members within the react-i18 ...