Angular Slider

This basic Angular slider component is malleable, responsive, and dynamic. Sometimes called a carousel too. There’s so many variations of how this can work. Use an API for the graphics and data. Add smooth transitions or image descriptions. However you decide you modify it, enjoy the feature.

Git Repo

Interface and TypeScript

This is a mirror of what the data looks like. Interfaces—were designed to describe object shapes and check for necessary keys. They’re virtual structures that exist solely in TypeScript at run time. Not thereafter simply because JavaScript doesn’t have such a feature. Fortunately for us, the object is rudimentary. More complex objects result in more complex interfaces. Though there are tools that convert data to interfaces, I prefer writing them myself.

  • unsubscribe is a private variable because no other class needs to know about it. We use it to end the subscription when the component life cycle is finished. This is denoted by the ngOnDestroy hook at the bottom.
  • result is our main array with the aforementioned interface reference.
  • currentIndex is our indice/ reference to the currently active book.
  • productInfo is whether the book description is shown or not
  • Set the private _http variable and be sure to add HttpClientModule to the parent module.
  • GET the response and store it in the results variable.
  • changeShowcase resets all the objects’ status’ values to false. Then sets the incoming value to true.
  • next and prevBook simply retrieve the current index of the original data stream, and reset the status values. Both of which are set via currentIndex++, subtract with currentIndex–, or set it to zero if we reach the end.
import { HttpClient } from '@angular/common/http';
import { Component } from '@angular/core';
import { Subject, takeUntil } from 'rxjs';
import { DataInterface } from './data.interface';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})

export class AppComponent {
  private unsubscribe$ = new Subject<void>;
  result: DataInterface[] = [];
  currentIndex: number = 0;
  productInfo: boolean = false;
  loaded: boolean = false;

  constructor(private _http: HttpClient) {}

  ngOnInit() {
    this._http.get<DataInterface[]>('path-to-json')
      .pipe(takeUntil(this.unsubscribe$))
      .subscribe((val) => {
        this.result = val;
        this.getSliderImage(this.currentIndex);
      });
  }

  changeShowcase(i: number) {
    this.currentIndex = i;
    this.result[this.currentIndex].url;
    this.resetValues();
    this.result[i].status = true;
  }

  getSliderImage(index: number) {
    this.loaded = true;
    return this.result[index].url;
  }

  nextBook() {
    this.getCurrentIndex();
    this.resetValues();
    this.currentIndex++;
    this.currentIndex > this.result.length - 1 ? (this.currentIndex = 0) : '';
    this.result[this.currentIndex].status = true;
  }

  prevBook() {
    this.getCurrentIndex();
    this.resetValues();
    this.currentIndex > 0 ? this.currentIndex-- : (this.currentIndex = this.result.length - 1);
    this.result[this.currentIndex].status = true;
  }

  getCurrentIndex() {
    for (var i = 0; i < this.result.length; i++) {
      if (this.result[i].status) this.currentIndex = i;
    }
    this.productInfo = false;
  }

  resetValues() {
    for (var index = 0; index < this.result.length; index++) {
      this.result[index].status = false;
    }
    this.productInfo = false;
  }

  ngOnDestroy(): void {
    this.unsubscribe$.next();
    this.unsubscribe$.complete();
  }
}

Angular Slider Markup and Style

The meat and potatoes are housed inside the slider ID element. If things are loaded, we show content. And if the image is not found, we add a new path to the image src element. All attributes come from the data stream.

Show the book description if productInfo is true. There’s a click function with the logic built right into the markup. I did this because it’s one simple variable toggle. Below that are the next and previous buttons. Both of which use the currentIndex variable. Below that are the radio buttons with a for loop. So, if we have more books, these will grow automatically.

<div class="wrapper element-shadow element-margin-top">
    <div class="slider-bg"></div>
    <div class="tagLine">Top Four AI Books of 2023</div>
    <div id="slider">
      <div class="content">
        <div *ngIf="loaded" class="books-wrapper">
          <img
            onerror="this.onerror=null;this.src='path-to-404-image'"
            [src]="getSliderImage(currentIndex)"
            class="element-shadow"
            alt="{{ result[currentIndex].altText }}"
          />
          <div
            class="product-info"
            [ngClass]="{ 'show-product-info': productInfo }"
          >
            {{ result[currentIndex].description }}
          </div>
          <div class="purchase-info">
            <div>
              <a
                href="{{ result[currentIndex].link }}"
                target="_blank"
                class="purchase-btn element-shadow"
                >Purchase Book</a
              >
              <div (click)="productInfo = !productInfo" class="info-wrapper">
                <span>ℹ</span>
              </div>
            </div>
          </div>
        </div>
      </div>
      <div class="navigation">
        <div (click)="prevBook()" class="prev"><span>➜</span></div>
        <div (click)="nextBook()" class="next"><span>➜</span></div>
      </div>
    </div>
    <div class="dots">
      <input
        type="radio"
        [checked]="item.status"
        name="arrPos"
        *ngFor="let item of result; let i = index"
        (click)="changeShowcase(i)"
      />
    </div>
  </div>

The styling has an auto generated gradient at the top with extra browser support. The purchaseInfo class is one big nested object for organizational purposes. Inside of the dots class, we’re changing the color of the radio buttons with accent. productInfo is the black overlay that slides in and out.

.wrapper {
  display: flex;
  height: 370px;
  border: 1px solid #313b3f;
  padding: 50px 0;
  position: relative;
  border-radius: 6px;
  overflow: hidden;
  background: rgb(49, 59, 63);
  background: -moz-linear-gradient(
    180deg,
    rgba(49, 59, 63, 1) 0%,
    rgba(255, 255, 255, 1) 100%
  );
  background: -webkit-linear-gradient(
    180deg,
    rgba(49, 59, 63, 1) 0%,
    rgba(255, 255, 255, 1) 100%
  );
  background: linear-gradient(
    180deg,
    rgba(49, 59, 63, 1) 0%,
    rgba(255, 255, 255, 1) 100%
  );
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#313b3f",endColorstr="#ffffff",GradientType=1);
}

#slider {
  position: relative;
  width: 100%;
  padding-top: 15px;
  margin: auto;
}

.slider-bg {
  background-color: #313b3f;
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  opacity: 0.4;
}

a { text-decoration: none; }

.purchase-info {
  text-align: center;
  > div {
    display: inline-block;
    width: 170px;
    .purchase-btn {
      background-color: #03658c;
      color: #FFFFFF;
      float: left;
      border-radius: 4px;
      padding: 8px 20px;
      font: normal 14px sans-serif;
      margin-top: 10px;
      width: 100px;
    }
    .info-wrapper {
      background-color: #313b3f;
      cursor: pointer;
      color: #FFFFFF;
      width: 20px;
      height: 20px;
      border-radius: 50%;
      float: right;
      position: relative;
      top: 17px;
      span {
        transform: translate(-50%, -50%);
        font-size: 16px;
        position: absolute;
        top: 50%;
        left: 50%;
      }
    }
  }
}

.tagLine {
  color: #FFFFFF;
  font: normal 18px sans-serif;
  border-bottom: 1px solid #FFFFFF;
  padding: 0;
  text-align: center;
  position: absolute;
  top: 10px;
  padding-bottom: 7px;
  right: 0;
  left: 0;
  margin: 0 auto;
}

.dots {
  text-align: center;
  border-top: 1px solid #313b3f;
  left: 0;
  right: 0;
  margin: 0 auto;
  padding: 5px 0;
  position: absolute;
  bottom: 0;

  input { cursor: pointer; }

  input[type='radio'] { accent-color: #03658c; }
}

.dots input[type='radio']:nth-child(2) { margin: 0 0 0 10px; }
.dots input[type='radio']:nth-child(3) { margin: 0 10px;     }

.content { position: relative;   }
.doctor-profiles { height: 100%; }

.books-wrapper {
  max-width: 200px;
  position: relative;
  margin: 0 auto;
  overflow: hidden;

  .product-info {
    background-color: rgba(0, 0, 0, 0.9);
    font: normal 14px sans-serif;
    height: calc(100% - 49px);
    box-sizing: border-box;
    transition: left 0.25s;
    position: absolute;
    border-radius: 4px;
    line-height: 20px;
    color: #FFFFFF;
    padding: 10px;
    width: 100%;
    top: 0;
    left: 110%;
  }

  img {
    width: 100%;
    border-radius: 4px;
  }
}

.show-product-info { left: 0 !important; }

.navigation {
  transform: translateY(-50%);
  font: bold 24px sans-serif;
  height: calc(100% - 80px);
  position: absolute;
  max-width: 380px;
  margin: 0 auto;
  overflow: auto;
  color: #FFFFFF;
  width: 100%;
  right: 0;
  left: 0;
  top: 50%;

  .prev {
    float: left;
    transform: rotate(-180deg);
    outline: none;
  }

  .next {
    float: right;
    outline: none;
  }

  > div:not(.info-wrapper) {
    height: 100%;
    cursor: pointer;
    > span {
      transform: translateY(-50%);
      position: relative;
      display: block;
      top: 50%;
    }
  }
}

@media screen and (max-width: 435px) {
  .navigation {
    margin: 0 15px;
    width: auto;
  }
}

In Conclusion

There’s so much more we can add here. Transitions between the imagery. We can add a description to each book and display it underneath vs on the book. We could add a star rating powered by Spring Boot so users of this carousel could rate the books. Or, we could even add params to the GET request. Send it to an Express backend to fetch the data. Regardless, I hope you found this useful and can use it in future projects. As is or customized to your liking. The list goes on. Check out my Angular Slider live code example.

Be the first to comment

Leave a Reply

Your email address will not be published.


*