Razvan Zamfir
Razvan Zamfir

Reputation: 4704

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

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 class="progress-bar">
   <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 class="player-container">
  <div id="audioPlayer">
    <span class="volume" @click="toggleMute">
      <i v-show="!muted" class="fa fa-volume-up"></i>
      <i v-show="muted" class="fa fa-volume-off"></i>
    </span>
    <div class="album">
      <div class="album-items">
        <div class="cover" :class="{'spinning' : isPlaying}"></div>
        <div class="info">
          <h1>{{tracks[trackCount].name}}</h1>
          <h2>{{tracks[trackCount].artistName}}</h2>
        </div>
      </div>
    </div>

    <div class="to-bottom">
      <div class="progress-bar">
        <span :style="{ width: trackProgress + '%' }"></span>
      </div>
      <div class="controls">
        <div class="navigate navigate-prev" :class="{'disabled' : trackCount == 0}" @click="prevTrack">
          <i class="fa fa-step-backward"></i>
        </div>
        <div class="navigate navigate-play" @click="playPause">
          <i v-show="!isPlaying" class="fa fa-play"></i>
          <i v-show="isPlaying" class="fa fa-pause"></i>
        </div>
        <div class="navigate navigate-next" :class="{'disabled' : trackCount == tracks.length - 1}" @click="nextTrack">
          <i class="fa fa-step-forward"></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?

Upvotes: 2

Views: 527

Answers (3)

connexo
connexo

Reputation: 56823

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

Upvotes: 1

Daniel Storey
Daniel Storey

Reputation: 943

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.

Upvotes: 3

Tadej Krevh
Tadej Krevh

Reputation: 442

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

Upvotes: 2

Related Questions