재사용 가능 로더 지시어 - Angular

목차
* 🚀 INTRO
* 🧪 EXAMPLE USE CASE
* ⚙ IMPLEMENTATION
* 🙏 THANK YOU

🚀 소개

Hi all! Hope you are all having a great weekend. I am currently working on multiple platforms using ReactJS, Angular 12 and Node.js.


I noticed that Angular is kinda hard to work with in terms of having reusable and easy to use components for loading, empty, error state and similar. So, I wanted to figure out the way of how to create a simple relative ("relative" in terms of where it is placed, which means it is placed in the relative parent element within the HTML)loader component that could just be plugged in into the component and control the loading of the specific component.

🧪 사용 사례 예

Let's consider this:

You are employee of the Stark industries. Tony rings you up:

Hey, man. Can you make a simple UI form that will allow me to select the suit type, color and reactor type. The form should be in the dialog?

Of course you would say yes, It is a freakin' Tony Stark xD

You start listing the requirements:

  • Dialog component

  • Should have 3 inputs, suit type, color and reactor type

  • We need to fetch available suits, colors and reactor (let's assume we can fetch it all from one endpoint)

  • The user should not be able to interact with the component unless everything is loaded

Let's see how to implement this!


⚙ [구현]

Our main dialog component (ts file) will have **status* class member which will tell us if the API call is triggered. It will also have a form group and of course the data that will provide us with the type of suits, colors and reactor types. Of course, we have to create our form and function that will call our API service. It will look something like this.

interface IDataResponse {
  suits: Array<any>;
  colors: Array<string>;
  reactors:Array<any>
}

@Component({
  selector: "stark-dialog",
  templateUrl: "./stark-dialogcomponent.html",
  styleUrls: ["./stark-dialog.component.scss"],
})
export class StarkDialogComponent implements OnInit {
  status: 'loading' | 'not-loading' = 'not-loading';
  starkForm!: FormGroup;
  data: Array<IDataResponse> = [];

  constructor(
    @Inject(MAT_DIALOG_DATA) public data: IDialogData,
    private _apiService: ApiService,
    private _formBuilder: FormBuilder,
  ) {
    this.createStarkForm();
  }

  ngOnInit(): void {
    this.status = 'loading';
    this.fetchData();
  }

  fetchData(): void {
    ...data fetching logic
    ...
    this.status = 'not-loading';
  }

  createStarkForm() {
    this.starkForm = this._formBuilder.group({
      selectSuitType: ["", Validators.required],
      selectColor: ["", Validators.required],
      selectReactorType: ["", Validators.required],
    });
  }
}

Our HTML could look something like this

<h2 mat-dialog-title>
  Choose suit setup
</h2>
<mat-dialog-content>
      <form [formGroup]="starkForm" (submit)="onSubmit()" fxLayout="column">
        <mat-form-field appearance="fill">
          <mat-label>Select suit type</mat-label>
          <mat-select name="selectSuitType">
            <mat-option *ngFor="let suit of data.suits" [value]="suit">
              {{ suit.type }}
            </mat-option>
          </mat-select>
        </mat-form-field>
        <mat-form-field appearance="fill">
          <mat-label>Select color</mat-label>
          <mat-select name="selectColor">
            <mat-option *ngFor="let color of data.colors" [value]="color">
              {{ color.key }}
            </mat-option>
          </mat-select>
        </mat-form-field>
        <mat-form-field appearance="fill">
          <mat-label>Select reactor type</mat-label>
          <mat-select name="selectReactorType">
            <mat-option *ngFor="let reactor of data.reactors" [value]="reactor">
              {{ reactor.type }}
            </mat-option>
          </mat-select>
        </mat-form-field>
      </form>
</mat-dialog-content>
<mat-dialog-actions align="end">
  <button mat-stroked-button color="primary" type="button" mat-dialog-close>
    Cancel
  </button>
  &nbsp;
  <button mat-raised-button color="primary">Submit</button>
</mat-dialog-actions>

Our form would then look something like this:



아이디어는 모든 데이터가 로드되기 전에 사용자가 이 양식을 보지 못하도록 하는 것입니다. Angular Directive를 사용하여 이에 대한 솔루션을 찾을 수 있습니다.

@Directive({
  selector: "[relativeLoader]",
})
export class RelativeLoaderDirective implements OnInit, OnChanges {
  private loader: HTMLElement;
  @Input() loading: boolean = false;
  constructor(private renderer: Renderer2, private el: ElementRef) {
    this.loader = this.renderer.createElement("div"); // create loader
  }
  ngOnInit(): void {}

  ngOnChanges(): void {
    this.createSimpleLoader(); // execute create loader
    if (this.loading && this.el) {
      // hide the first element in the parent div containing directive
      // this should always be a component you want to replace with
      // the loader we are making
      this.renderer.setStyle(
        this.el.nativeElement.firstChild,
        "display",
        "none"
      );
      this.renderer.appendChild(this.el.nativeElement, this.loader);
    } else {
      this.renderer.removeChild(this.el.nativeElement, this.loader);
      this.renderer.setStyle(
        this.el?.nativeElement.firstChild,
        "display",
        "block"
      );
    }
  }

  createSimpleLoader() {
    /** add some style to the loader wrapper */
    this.renderer.setStyle(this.loader, "display", "flex");
    this.renderer.setStyle(this.loader, "flex-direction", "column");
    this.renderer.setStyle(this.loader, "justify-content", "center");
    this.renderer.setStyle(this.loader, "align-items", "center");
    // create loader spinner with custom scss
    /** Format of this loader is:
        <div class="lds-roller">
            <div></div>
            <div></div>
            <div></div>
            <div></div>
                <div></div>
                <div></div>
                <div></div>
               <div></div>
        </div>
    */
    const ldsRoller = this.renderer.createElement("div");
    this.renderer.addClass(ldsRoller, "lds-roller");
    [0, 1, 2, 3, 4, 5, 6, 7].forEach((value) => {
      const div = this.renderer.createElement("div");
      this.renderer.appendChild(ldsRoller, div);
    });
    this.renderer.appendChild(this.loader, ldsRoller);
  }
}


로더용 SCSS(https://loading.io/css/에서 가져옴)

.lds-roller {
  display: inline-block;
  position: relative;
  width: 80px;
  height: 80px;
}
.lds-roller div {
  animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
  transform-origin: 40px 40px;
}
.lds-roller div:after {
  content: " ";
  display: block;
  position: absolute;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #fff;
  margin: -4px 0 0 -4px;
}
.lds-roller div:nth-child(1) {
  animation-delay: -0.036s;
}
.lds-roller div:nth-child(1):after {
  top: 63px;
  left: 63px;
}
.lds-roller div:nth-child(2) {
  animation-delay: -0.072s;
}
.lds-roller div:nth-child(2):after {
  top: 68px;
  left: 56px;
}
.lds-roller div:nth-child(3) {
  animation-delay: -0.108s;
}
.lds-roller div:nth-child(3):after {
  top: 71px;
  left: 48px;
}
.lds-roller div:nth-child(4) {
  animation-delay: -0.144s;
}
.lds-roller div:nth-child(4):after {
  top: 72px;
  left: 40px;
}
.lds-roller div:nth-child(5) {
  animation-delay: -0.18s;
}
.lds-roller div:nth-child(5):after {
  top: 71px;
  left: 32px;
}
.lds-roller div:nth-child(6) {
  animation-delay: -0.216s;
}
.lds-roller div:nth-child(6):after {
  top: 68px;
  left: 24px;
}
.lds-roller div:nth-child(7) {
  animation-delay: -0.252s;
}
.lds-roller div:nth-child(7):after {
  top: 63px;
  left: 17px;
}
.lds-roller div:nth-child(8) {
  animation-delay: -0.288s;
}
.lds-roller div:nth-child(8):after {
  top: 56px;
  left: 12px;
}
@keyframes lds-roller {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}



중요한 것!

이것이 작동하려면 다음 형식의 HTML을 제공해야 합니다.

<div relativeLoader [loading]="your_loading_indicator">
    <div>
      <content-you-want-to-replace-with-loader />
    </div>
</div>


작동 방식:

Directive는 그것이 참조하는 상위 참조를 찾을 것입니다. 첫 번째 div인 첫 번째 자식을 찾고 숨기고 부모 요소 끝에 로더를 추가합니다.

HTML을 업데이트합시다

<h2 mat-dialog-title>
  Choose suit setup
</h2>
<mat-dialog-content>
  <div relativeLoader [loading]="isLoading === 'loading'">
     <div>
      <form [formGroup]="starkForm" (submit)="onSubmit()" fxLayout="column">
        <mat-form-field appearance="fill">
          <mat-label>Select suit type</mat-label>
          <mat-select name="selectSuitType">
            <mat-option *ngFor="let suit of data.suits" [value]="suit">
              {{ suit.type }}
            </mat-option>
          </mat-select>
        </mat-form-field>
        <mat-form-field appearance="fill">
          <mat-label>Select color</mat-label>
          <mat-select name="selectColor">
            <mat-option *ngFor="let color of data.colors" [value]="color">
              {{ color.key }}
            </mat-option>
          </mat-select>
        </mat-form-field>
        <mat-form-field appearance="fill">
          <mat-label>Select reactor type</mat-label>
          <mat-select name="selectReactorType">
            <mat-option *ngFor="let reactor of data.reactors" [value]="reactor">
              {{ reactor.type }}
            </mat-option>
          </mat-select>
        </mat-form-field>
      </form>
  </div>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
  <button mat-stroked-button color="primary" type="button" mat-dialog-close>
    Cancel
  </button>
  &nbsp;
  <button mat-raised-button color="primary">Create</button>
</mat-dialog-actions>


끝을 추가하면 다음과 같은 결과를 얻을 수 있습니다.



따라서 걱정해야 할 유일한 사항은 로드 상태를 제어할 클래스 멤버가 있다는 것입니다. 그러나 제공된 템플릿을 사용하는 경우 전체 애플리케이션에서 이 로더 지시문을 재사용할 수 있어야 합니다.

🙏 읽어주셔서 감사합니다!

Please leave a comment, tell me about you, about your work, comment your thoughts, connect with me!

☕ SUPPORT ME AND KEEP ME FOCUSED!



즐거운 해킹 시간 되세요! 😊

좋은 웹페이지 즐겨찾기