Search code examples
angularbabeljsstylus

Converting CodePen babel and stylus to Angular 5 and scss


I've found this neat CodePen animated button group :

https://codepen.io/Zaku/pen/reGRBg

And I'm trying to implement it into my angular 5 project.

I got pretty far on it. I can see somethings's happening with the svg but it isn't transitioning and it's a black shadow :

enter image description here

Here are the changes that I've made :

mycomponent.ts :

import {Component, ElementRef, OnInit} from '@angular/core';

@Component({
  selector: 'app-mycomponent',
  templateUrl: './mycomponent.html',
  styleUrls: ['./mycomponent.scss']
})
export class myComponent implements OnInit {

  pathLength = 68 * 2 + 200;
  group;
  buttons;
  svg;
  path;
  currentPath;
  currentIndex;

  constructor(private elRef:ElementRef) {}

  ngOnInit() {
    this.group = this.elRef.nativeElement.querySelector('.btn-group');
    this.buttons = Array.prototype.slice.call(
      this.group.querySelectorAll(".btn")
    );
    console.log(this.buttons);
    this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    this.svg.setAttribute(
      "viewbox",
      `-1 -1 ${160 * this.buttons.length + 2} 42`
    );
    this.path = document.createElementNS("http://www.w3.org/2000/svg", "path");
    this.currentPath = "M -80, 40";
    this.currentIndex = -1;
    this.activateIndex(
      this.buttons.indexOf(this.group.querySelector(".active"))
    );
    this.group.appendChild(this.svg);
    this.svg.appendChild(this.path);
    this.refreshPath();
  }

  onClick(e) {
    const index = this.buttons.indexOf(e.srcElement || e.target);
    this.activateIndex(index);
  }

  refreshPath() {
    this.path.setAttributeNS(null, "d", this.currentPath);
    this.path.style.strokeDashoffset =
      (-this.path.getTotalLength() + this.pathLength) * 0.9965;
  }

  center(index) {
    return index * 160 + 80;
  }

  removeClass(str) {
    if (this.buttons[this.currentIndex]) {
      this.buttons[this.currentIndex].classList.remove(str);
    }
  }

  addClass(str) {
    if (this.buttons[this.currentIndex]) {
      this.buttons[this.currentIndex].classList.add(str);
    }
  }

  activateIndex(index) {
    const lastCenter = this.center(this.currentIndex);
    const nextCenter = this.center(index);
    const offset = index < this.currentIndex ? -50 : 50;
    const curve = index < this.currentIndex ? -30 : 30;
    this.currentPath += `
    L ${lastCenter + offset}, 40
    C ${lastCenter + offset + curve}, 40
      ${lastCenter + offset + curve},  0
      ${lastCenter + offset},  0
    L ${lastCenter - offset},  0
    C ${lastCenter - offset - curve},  0
      ${lastCenter - offset - curve}, 40
      ${lastCenter - offset}, 40
    L ${nextCenter + offset}, 40
    C ${nextCenter + offset + curve}, 40
      ${nextCenter + offset + curve},  0
      ${nextCenter + offset},  0
    L ${nextCenter - offset},  0
    C ${nextCenter - offset - curve},  0
      ${nextCenter - offset - curve}, 40
      ${nextCenter - offset}, 40
    L ${nextCenter}, 40`;
    this.removeClass("active");
    this.currentIndex = index;
    this.addClass("active");
    this.refreshPath();
  }
}

mycomponent.html : (you'll notice I added angular click watchers to be the ones to handle the actions instead)

<div class="container">
  <div class="btn-group">
    <div (click)="onClick($event)" class="btn">First</div>
    <div (click)="onClick($event)" class="btn active">Middle</div>
    <div (click)="onClick($event)" class="btn">Last</div>
  </div>
</div>

mycomponent.scss:

$easing: cubic-bezier(0, 0.8, 0.2, 1);
$duration: 1s;

  .container {
    color: #19cc95;
    text-align: center;
  }

  .btn-group {
    position: relative;
    display: inline-block;
    .btn {
      cursor: pointer;
      float: left;
      height: 40px;
      width: 160px;
      line-height: 40px;
      text-align: center;
      -webkit-user-select: none;
      transition: font-size 0.3s ease;
      &:active {
        font-size: 0.8em;
      }
    }
    svg {
      z-index: -1;
      top: 0;
      left: 0;
      position: absolute;
      width: 100%;
      height: 100%;
      overflow: visible;
    }
    path {
      fill: none;
      stroke: #19cc95;
      stroke-dasharray: 334.9, 99999;
      transition: stroke-dashoffset 1s cubic-bezier(0, 0.8, 0.2, 1);
      stroke-width: 1;
    }
  }

the above isn't half bad. the 'active' class is being correctly set on clicks. and the svg black shadows change.

obviously the svg is supposed to be overlayed onto the menus and that isn't happening.

what am I missing?


Solution

  • Since svg element is dynamically created, you'll have to use encapsulation: ViewEncapsulation.None on the component that dynamically creates svg to make sure styles from .component.scss are applied to everything.

    I copied your code and ran it successfully, but with a little modification.

    component.html

    <div class="container">
      <div #btnGroup class="btn-group">
        <div class="btn" (click)="onClick($event)">First</div>
        <div class="btn active" (click)="onClick($event)">Middle</div>
        <div class="btn" (click)="onClick($event)">Last</div>
      </div>
    </div>
    

    component.ts

    import {Component, ElementRef, OnInit, ViewChild, ViewEncapsulation} from '@angular/core';
    
    @Component({
      selector: 'app-svg-buttons',
      templateUrl: './svg-buttons.component.html',
      styleUrls: ['./svg-buttons.component.scss'],
      encapsulation: ViewEncapsulation.None
    })
    export class SvgButtonsComponent implements OnInit, AfterViewInit {
      @ViewChild('btnGroup') btnGroup: ElementRef;
    
      pathLength = 68 * 2 + 200;
      group;
      buttons;
      svg;
      path;
      currentPath;
      currentIndex;
    
      constructor(private elementRef: ElementRef) {
      }
    
      ngOnInit() {
      }
    
      ngAfterViewInit() {
        this.initializeGroup(this.btnGroup.nativeElement);
      }
    
      initializeGroup(group) {
        this.group = group;
    
        this.buttons = Array.prototype.slice.call(
          this.group.querySelectorAll('.btn')
        );
    
        this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        this.svg.setAttribute('viewbox',
          `-1 -1 ${160 * this.buttons.length + 2} 42`
        );
        this.path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        this.currentPath = 'M -80, 40';
        this.currentIndex = -1;
        this.activateIndex(
          this.buttons.indexOf(
            this.group.querySelector('.active')
          )
        );
        this.group.appendChild(this.svg);
        this.svg.appendChild(this.path);
        this.refreshPath();
      }
    
      onClick(e) {
        const index = this.buttons.indexOf(e.srcElement || e.target);
        this.activateIndex(index);
      }
    
      refreshPath() {
        this.path.setAttributeNS(null, 'd', this.currentPath);
        this.path.style.strokeDashoffset = (-this.path.getTotalLength() + this.pathLength) * 0.9965;
      }
    
      center(index) {
        return index * 160 + 80;
      }
    
      removeClass(str) {
        if (this.buttons[this.currentIndex]) {
          this.buttons[this.currentIndex].classList.remove(str);
        }
      }
    
      addClass(str) {
        if (this.buttons[this.currentIndex]) {
          this.buttons[this.currentIndex].classList.add(str);
        }
      }
    
      activateIndex(index) {
        const lastCenter = this.center(this.currentIndex);
        const nextCenter = this.center(index);
        const offset = index < this.currentIndex ? -50 : 50;
        const curve = index < this.currentIndex ? -30 : 30;
        this.currentPath += `
          L ${lastCenter + offset        }, 40
          C ${lastCenter + offset + curve}, 40
            ${lastCenter + offset + curve},  0
            ${lastCenter + offset        },  0
          L ${lastCenter - offset        },  0
          C ${lastCenter - offset - curve},  0
            ${lastCenter - offset - curve}, 40
            ${lastCenter - offset        }, 40
          L ${nextCenter + offset        }, 40
          C ${nextCenter + offset + curve}, 40
            ${nextCenter + offset + curve},  0
            ${nextCenter + offset        },  0
          L ${nextCenter - offset        },  0
          C ${nextCenter - offset - curve},  0
            ${nextCenter - offset - curve}, 40
            ${nextCenter - offset        }, 40
          L ${nextCenter                 }, 40
        `;
        this.removeClass('active');
        this.currentIndex = index;
        this.addClass('active');
        this.refreshPath();
      }
    };