Home > Software engineering >  Why causes the failure to bind a computed property as inline style in this Vue 3 app?
Why causes the failure to bind a computed property as inline style in this Vue 3 app?

Time:12-27

I am working on an audio player with Vue 3 and the Napster API.

Project details

The player has a progress bar. I use the trackProgress computed property to update the progress in real-time:

<div >
   <span :style="{ width: trackProgress   '%' }"></span>
 </div>

const musicApp = {
  data() {
    return {
      player: new Audio(),
      trackCount: 0,
      tracks: [],
      muted: false,
      isPlaying: false
    };
  },
  methods: {
    async getTracks() {
      try {
        const response = await axios
          .get(
            "https://api.napster.com/v2.1/tracks/top?apikey=ZTk2YjY4MjMtMDAzYy00MTg4LWE2MjYtZDIzNjJmMmM0YTdm"
          )
          .catch((error) => {
            console.log(error);
          });
        this.tracks = response;
        this.tracks = response.data.tracks;
      } catch (error) {
        console.log(error);
      }
    },
    nextTrack() {
      if (this.trackCount < this.tracks.length - 1) {
        this.trackCount  ;
        this.setPlayerSource();
        this.playPause();
      }
    },
    prevTrack() {
      if (this.trackCount >= 1) {
        this.trackCount--;
        this.setPlayerSource();
        this.playPause();
      }
    },
    setPlayerSource() {
      this.player.src = this.tracks[this.trackCount].previewURL;
    },
    playPause() {
      if (this.player.paused) {
        this.isPlaying = true;
        this.player.play();
      } else {
        this.isPlaying = false;
        this.player.pause();
      }
    },
    toggleMute() {
      this.player.muted = !this.player.muted;
      this.muted = this.player.muted;
    }
  },
  async created() {
    await this.getTracks();
    this.setPlayerSource();
    this.player.addEventListener("ended", () => {
      this.isPlaying = false;
    });
  },
  computed: {
    trackProgress() {
      this.player.addEventListener("loadedmetadata", () => {
        return (this.player.currentTime / this.player.duration) * 100;
      });
    }
  }
};

Vue.createApp(musicApp).mount("#audioPlayer");
html,
body {
  margin: 0;
  padding: 0;
  font-size: 16px;
}

body * {
  box-sizing: border-box;
  font-family: "Montserrat", sans-serif;
}

@-webkit-keyframes spin {
  0% {
    -webkit-transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(360deg);
  }
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

.player-container {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  background-color: #2998ff;
  background-image: linear-gradient(62deg, #2998ff 0%, #5966eb 100%);
}

#audioPlayer {
  width: 300px;
  height: 300px;
  border-radius: 8px;
  position: relative;
  overflow: hidden;
  background-color: #00ca81;
  background-image: linear-gradient(160deg, #00ca81 0%, #ffffff 100%);
  box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
  display: flex;
  flex-direction: column;
  align-items: center;
}

.volume {
  color: #ff0057;
  opacity: 0.9;
  display: inline-block;
  width: 20px;
  font-size: 20px;
  position: absolute;
  top: 5px;
  right: 6px;
  cursor: pointer;
}

.album {
  width: 100%;
  flex: 1;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
}

.album-items {
  padding: 0 10px;
  text-align: center;
}

.cover {
  width: 150px;
  height: 150px;
  margin: auto;
  box-shadow: 0px 5px 12px 0px rgba(0, 0, 0, 0.17);
  border-radius: 50%;
  background: url("https://w7.pngwing.com/pngs/710/955/png-transparent-vinyl-record-artwork-phonograph-record-compact-disc-lp-record-disc-jockey-symbol-miscellaneous-classical-music-sound.png") center top transparent;
  background-size: cover;
}

.cover.spinning {
  webkit-animation: spin 6s linear infinite;
  /* Safari */
  animation: spin 6s linear infinite;
}

.info {
  width: 100%;
  padding-top: 5px;
  color: #000;
  opacity: 0.85;
}

.info h1 {
  font-size: 11px;
  margin: 5px 0 0 0;
}

.info h2 {
  font-size: 10px;
  margin: 3px 0 0 0;
}

.to-bottom {
  width: 100%;
  margin-top: auto;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
}

.progress-bar {
  background-color: #ff0057;
  opacity: 0.9;
  height: 3px;
  width: 100%;
}

.progress-bar span {
  display: block;
  height: 3px;
  width: 0;
  background: rgba(255, 255, 255, 0.4);
}

.controls {
  width: 150px;
  display: flex;
  height: 60px;
  justify-content: space-between;
  align-items: center;
}

.controls .navigate {
  display: flex;
  box-shadow: 1px 2px 7px 2px rgba(0, 0, 0, 0.09);
  width: 33px;
  height: 33px;
  line-height: 1;
  color: #ff0057;
  cursor: pointer;
  background: #fff;
  opacity: 0.9;
  border-radius: 50%;
  text-align: center;
  justify-content: center;
  align-items: center;
}

.controls .navigate.disabled {
  pointer-events: none;
  color: #606060;
  background-color: #f7f7f7;
}

.controls .navigate.navigate-play {
  width: 38px;
  height: 38px;
}

.navigate-play .fa-play {
  margin-left: 3px;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<script src="https://unpkg.com/[email protected]/dist/axios.min.js"></script>
<script src="https://unpkg.com/vue@next"></script>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;500&display=swap" rel="stylesheet">

<div >
  <div id="audioPlayer">
    <span  @click="toggleMute">
      <i v-show="!muted" ></i>
      <i v-show="muted" ></i>
    </span>
    <div >
      <div >
        <div  :></div>
        <div >
          <h1>{{tracks[trackCount].name}}</h1>
          <h2>{{tracks[trackCount].artistName}}</h2>
        </div>
      </div>
    </div>

    <div >
      <div >
        <span :style="{ width: trackProgress   '%' }"></span>
      </div>
      <div >
        <div  : @click="prevTrack">
          <i ></i>
        </div>
        <div  @click="playPause">
          <i v-show="!isPlaying" ></i>
          <i v-show="isPlaying" ></i>
        </div>
        <div  : @click="nextTrack">
          <i ></i>
        </div>
      </div>
    </div>
  </div>
</div>

The problem

For a reason I have not been able to figure out, the style is not binded to the span element inside progress-bar.

What have I missed?


UPDATE

Using setInterval inside the created hook, works, but I would rather avoid it?

this.player.addEventListener("loadedmetadata", () => {
  setInterval(() => {
    this.trackProgress =
      (this.player.currentTime / this.player.duration) * 100;
  }, 100);
});

What's a better alternative?

CodePudding user response:

Description of the solution process:

  1. setup a listener for the player's timeupdate event
  2. update the component's data when timeupdate fires
  3. use a percentageProgress computed property to calculate the progress and use this in the template. (You could still use your trackProgress property but percentageProgress is a bit clearer semantically.)

Implementation:

data() {
  return {
    player: new Audio(),
    trackCount: 0,
    tracks: [],
    muted: false,
    isPlaying: false,
    currentTime: 0
  };
},

computed: {
  percentageProgress() {
    return (this.currentTime / this.player.duration) * 100;
  }
}

created() {
  this.player.addEventListener("timeupdate", () => {
    this.currentTime = this.player.currentTime;
  });
}

On another note, computed getters must return a value. Your computed property doesn't return anything.

CodePudding user response:

You should create a data property trackProgress and update it in the listener which you create in created() hook (similar to ended event).

CodePudding user response:

You keep adding event listeners in your computed property, which makes no sense at all.

Also these listeners return the value you want, but these just go into the digital nirvana because noone can take the return value when the event occurs and do something with it.

Instead of a computed property, add a trackProgress property in your data() model, and move the adding of the event listener to the mounted part, so it's only added once.

That listener will then update your model property trackProgress.

data() { return {
  player: new Audio(),
  trackCount: 0,
  tracks: [],
  muted: false,
  isPlaying: false,
  trackProgress: 0,
} }

and

mounted: function () {
  this.player.addEventListener("timeupdate", () => {
    this.trackProgress = (this.player.currentTime / this.player.duration) * 100;
  });
}

CodePudding user response:

You clearly don't know what you doing. Let me try to explain.

Why first solution was wrong:

Computed property is synchronous thing. So when it is called it allways expect to return a value right away. That's not what is happening in your code because this part of code

() => {
        return (this.player.currentTime / this.player.duration) * 100;
      }

is called when player sends event loadedmetadata. And that will always happen after some time. You therefore allways returned nothing when this computed was called.

Why Second (UPDATE) solution is wrong

When you add event listener in interval, it's gonna register new listener every 100ms, so after a while there will be thounsands, milions, infinite needless changes on this.trackProgress and your machine will burst in flames. You need only one listener. What were you looking for was probably setTimeout instead of setInterval. But even this is wrong solution I guess. I think the reason why this isn't working without some wait time is because this.player doesn't exist yet. Try use mounted() hook instead. DOM elements should be ready in mounted hook.

  • Related