קודקודייל
  • קודקודייל
  • מי אתם קודקודייל?
  • קורסים בחינם
  • צרו קשר
  • בניית אתרים
    • וורדפרס
  • נגישות אתרים
  • כל הקטגוריות
    • אנגולר
    • HTML
    • CSS
    • Javascript
    • Typescript
    • NodeJs
    • בלוקציין
  • קודקודייל
  • מי אתם קודקודייל?
  • קורסים בחינם
  • צרו קשר
  • בניית אתרים
    • וורדפרס
  • נגישות אתרים
  • כל הקטגוריות
    • אנגולר
    • HTML
    • CSS
    • Javascript
    • Typescript
    • NodeJs
    • בלוקציין
קודקודייל
  • קודקודייל
  • מי אתם קודקודייל?
  • קורסים בחינם
  • צרו קשר
  • בניית אתרים
    • וורדפרס
  • נגישות אתרים
  • כל הקטגוריות
    • אנגולר
    • HTML
    • CSS
    • Javascript
    • Typescript
    • NodeJs
    • בלוקציין
  • קודקודייל
  • מי אתם קודקודייל?
  • קורסים בחינם
  • צרו קשר
  • בניית אתרים
    • וורדפרס
  • נגישות אתרים
  • כל הקטגוריות
    • אנגולר
    • HTML
    • CSS
    • Javascript
    • Typescript
    • NodeJs
    • בלוקציין
ראשי ♦ אנגולר ♦ רכיבים: בניה של רכיב MULTI-SELECT נגיש באנגולר

רכיבים: בניה של רכיב MULTI-SELECT נגיש באנגולר

עידן יצחקי 22 באוקטובר 2021 אין תגובות

מיד נראה איך לבנות רכיב אנגולר בסיסי שיתפקד כ-multi-select והוא יעבוד כרכיב ב-reactive form וכרכיב ב-template form.

בנוסף, נבנה אותו כך שהוא יהיה נגיש לשימוש במקלדת ובקורא מסך.

תחילה אני רוצה להבהיר שניתן לממש את הרכיב במספר צורות, אבל יש מספר דברים שחייבים להיות ברכיב בלי קשר לצורת המימוש שלו.

נראה איך הוא בנוי ולאחר מכן נעבור על הדברים החשובים

הרכיב:

<div #selectWrapper class="multi-select" [ngClass]="{'select-active':selectActive==true}">
    <button class="select-main-btn shadow-outline" [id]="dataId" #btnMain type="button"
        (click)="selectActive=!selectActive;" (blur)="onListBlur($event);" role="listbox"
        [attr.aria-expanded]="selectActive" aria-haspopup="listbox"
        [attr.aria-label]="selectedSumText">{{selectedSumText}}
        <img src="assets/icons/Arrow_Right.svg" alt="arrow right" aria-hidden="true" class="img-arrow-indicator">
    </button>
    <div class="select-list-hide" [ngClass]="{'select-list-active':selectActive==true}">
        <ul class="select-list">
            <li *ngFor="let dataItem of privateData;trackBy:trackByFnc" role="option">
                <label class="select-item" tabindex="-1" id="{{dataId}}label{{dataItem.id}}">
                    <div class="select-item-checkbox-wrapper">
                        <input type="checkbox" class="select-item-checkbox shadow-outline" [id]="dataId+dataItem.id"
                            [value]="dataItem.itemValue" (click)="onItemSelected(dataItem)" (blur)="onListBlur($event);"
                            (keyup)="escFn($event)">
                        <div class="checkbox-state"></div>
                    </div>
                    <img class="select-item-img" [src]="dataItem.imgSrc" [alt]="dataItem.imgAlt">
                    <span class="select-item-text">{{dataItem.itemText}}</span>
                </label>
            </li>
        </ul>
    </div>
</div>
import { Component, OnInit, Input, Output, EventEmitter, ViewChild, ElementRef, forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';
import { IMultiSelect } from './multi-select.interface';

@Component({
  selector: 'app-multi-select',
  templateUrl: './multi-select.component.html',
  styleUrls: ['./multi-select.component.css'],
  providers: [
    { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MultiSelectComponent), multi: true }
  ]
})
export class MultiSelectComponent implements OnInit, ControlValueAccessor {
  @Input() dataId: string = '';
  validateFn: any = () => { };
  @Input() selectData: IMultiSelect[] = [];
  privateData: IMultiSelect[] = [];
  userSelectedData: IMultiSelect[] = [];
  @Input() validation: boolean = false;
  @Output() selectedValueEmitter: EventEmitter<Array<string>> = new EventEmitter<Array<string>>();
  @Output() isValidEmitter: EventEmitter<boolean> = new EventEmitter<boolean>();
  @ViewChild('selectWrapper', { static: false }) selectWrapperRef: ElementRef;
  @ViewChild('btnMain', { static: false }) btnMainRef: ElementRef;
  selectActive: boolean = false;
  selectedSumText: string = "";
  constructor() { }


  generateId(item: IMultiSelect): string {
    if (item.id === undefined || item.id === "") {
      return item.itemValue;
    }
    else
      return item.id;
  }
  trackByFnc(index, item) {
    if (!item) return null;
    if (item.id) {
      return item.id;
    }
    if (item.itemValue) {
      return item.itemValue;
    }
    return index;
  }
  ngOnInit() {
    if (typeof (this.selectData) !== "undefined") {
      if (this.selectData.length) {
        this.privateData = JSON.parse(JSON.stringify(this.selectData));
        for (let i = 0; i < this.privateData.length; i++) {
          this.privateData[i].id = this.generateId(this.privateData[i]);
        }
      }
    }
  }
  writeValue(value: any) {
    if (value !== undefined) {
    }
  }
  propagateChange = (_: any) => { };
  registerOnChange(fn) {
    this.propagateChange = fn;
  }
  registerOnTouched() { }
  onItemSelected(item: IMultiSelect) {
    if (this.userSelectedData.length > 0) {
      let indexFound = this.userSelectedData.findIndex(localItem => localItem.id === item.id);
      if (indexFound > -1) {
        this.userSelectedData.splice(indexFound, 1);
      }
      else {
        this.userSelectedData.push(item);
      }
    }
    else {
      this.userSelectedData.push(item);
    }
    if (this.userSelectedData.length === 0) {
      this.selectedSumText = '';
    }
    else {
      this.selectedSumText = this.userSelectedData.length + ' items selected';
    }


  }
  closingUp() {
    this.selectActive = false;
    let arrayValues = this.userSelectedData.map(item => { return item.itemValue });
    this.selectedValueEmitter.emit(arrayValues);
    this.propagateChange(arrayValues);
    this.btnMainRef.nativeElement.focus();
  }
  onListBlur(event) {
    if (this.selectWrapperRef.nativeElement.contains(event.relatedTarget) === false) {
      if (this.validation === true) {
        if (this.selectedSumText !== "") {
          this.isValidEmitter.emit(true);
        }
        else {
          this.isValidEmitter.emit(false);
        }
      }
      else {
        this.isValidEmitter.emit(true);
      }
      if (this.selectActive === true) {
        this.closingUp();
      }

    }
  }
  escFn(event) {
    if (event.which === 27 || event.code === "Escape") {
      this.closingUp();
    }
  }
}
.multi-select {
    position: relative;
}

.multi-select .select-main-btn {
    padding: 0;
    border: 1px solid rgb(190, 190, 190);
    background-color: transparent;
    min-width: 100px;
    min-height: 30px;
    width: 100%;
    position: relative;
    overflow: hidden;
    padding-right: 34px;
    text-align: left;
    padding-left: 10px;
    border-radius: 4px;
    -webkit-border-radius: 4px;
    -moz-border-radius: 4px;
    -ms-border-radius: 4px;
    -o-border-radius: 4px;
    cursor: pointer;
    box-sizing: border-box;
}

.multi-select .select-main-btn .img-arrow-indicator {
    position: absolute;
    top: 0;
    bottom: 0;
    right: 0;
    height: 100%;
    transform: rotate(0deg);
    -webkit-transform: rotate(0deg);
    -moz-transform: rotate(0deg);
    -ms-transform: rotate(0deg);
    -o-transform: rotate(0deg);
    transition: all 0.5s;
    -webkit-transition: all 0.5s;
    -moz-transition: all 0.5s;
    -ms-transition: all 0.5s;
    -o-transition: all 0.5s;
}

.multi-select.select-active .select-main-btn .img-arrow-indicator {
    transform: rotate(90deg);
    -webkit-transform: rotate(90deg);
    -moz-transform: rotate(90deg);
    -ms-transform: rotate(90deg);
    -o-transform: rotate(90deg);
}

.multi-select .select-list {
    list-style-type: none;
    padding: 0;
    margin: 0;
    width: 100%;
    border: 1px solid #818181;
    position: absolute;
    z-index: 999999;
    top: 100%;
    left: 0;
    max-height: 168px;
    overflow-y: auto;
    box-sizing: border-box;
}

.multi-select .select-list .select-item {
    padding: 6px 25px;
    width: 100%;
    display: flex;
    border: 0;
    border-bottom: 1px solid #b8b8b8;
    text-align: left;
    background-color: #ededed;
    word-break: break-all;
    cursor: pointer;
    box-sizing: border-box;
}

.multi-select .select-list .select-item:hover,
.multi-select .select-list .select-item:focus {
    background-color: #f1f1f1;
}

.multi-select .select-list-hide {
    display: none;
}

.multi-select .select-list-hide.select-list-active {
    display: block;
}

.multi-select .select-item[aria-selected="true"] {
    font-weight: bold;
    position: relative;
}

.multi-select .select-item[aria-selected="true"]::before {
    content: url(../../../../assets/icons/check.svg);
    position: absolute;
    left: 5px;
    top: 7px;
    bottom: 6px;
    min-height: 15px;
    min-width: 15px;
}

.multi-select .select-item .select-item-img {
    max-width: 20px;
    max-height: 20px;
    vertical-align: middle;
    margin: 0 8px 0 0;
    display: inline-block;
}

.multi-select .select-item .select-item-checkbox {
    width: 100%;
    height: 100%;
    margin: 0;
    cursor: pointer;
    background-image: none;
    background-color: transparent;
}

.multi-select .select-item .select-item-checkbox-wrapper {
    display: inline-block;
    vertical-align: middle;
    width: 20px;
    height: 20px;
    margin: 0 8px 0 0;
    position: relative;
}

.multi-select .select-item .select-item-checkbox-wrapper .select-item-checkbox+.checkbox-state::before {
    content: url("/assets/icons/checkbox-unchecked.svg");
    position: absolute;
    top: 0;
    left: 0;
    width: 20px;
    height: 20px;
    background-color: #ffffff;
}

.multi-select .select-item .select-item-checkbox-wrapper .select-item-checkbox:checked+.checkbox-state::before {
    content: url("/assets/icons/checkbox-checked.svg");
}

.multi-select .select-item .select-item-text {
    display: inline-block;
}
export interface IMultiSelect{
    id?:string,
    itemText:string,
    itemValue:string,
    imgSrc?:string,
    imgAlt?:string
}

הדברים שחובה לשים בכל רכיב שהולך לתקשר עם טופס של אנגולר הם:

  • ControlValueAccessor
  • NG_VALUE_ACCESSOR
  •  writeValue(value: any)
  • propagateChange = (_: any)
  • registerOnChange(fn)
  • registerOnTouched()

כאשר אנחנו שמים רכיב בטופס אנגולר וזה לא משנה אם הוא טמפלייט או ריאקטיב אנחנו צריכים שהרכיב יתקשר עם הטופס,

כל הרשימה למעלה נועדה לתת לרכיב שלנו לתקשר עם הטופס.

נתחיל בזה שברגע שתממשו את ControlValueAccessor הקומפיילר לא יפסיק לעלות שגיאות עד שתממשו את כל הפונקציות שרשומות למעלה (למעט NG_VALUE_ACCESSOR), גם אם הפונקציות ריקות ולא עושות כלום.

ControlValueAccessor הוא הגשר בין ה-DOM (HTML) לבין ה-API של אנגולר.

writeValue היא פונקציה שתפקידה לקבל נתון מבחוץ ולהעביר אותו למשתנה בתוך הרכיב שלנו,

הנתון מבחוץ לא מגיע כ-INPUT , אלא הוא מגיע מה-API של אנגולר.

במקרה של טופס טמפלייט הוא מגיע מה-ngModel, במקרה של ריאקטיב הוא מגיע מה-FormBuilder במידה ויש שם ערך.

propagateChange תפקידו לעדכן את העולם החיצון מהרכיב שמשהו השתנה בו.

registerOnChange פונקציה שתרשום את העדכון של propagateChange .

registerOnTouched פונקציה שמופעלת כאשר הרכיב מאבד פוקוס, ניתן להוסיף לוגיקה בתוך הפונקציה שתרוץ כל פעם שהמשתמש סימן את הרכיב ועבר לרכיב אחר.

NG_VALUE_ACCESSOR התפקיד שלו פשוט מאוד, כל מה שהוא עושה זה לרשום את הרכיב שלנו כ-form control וכך ה-form יכיר וידע איך לתקשר עם הרכיב שלנו.

כל שאר הפונקציות והמשתנים הם מימוש שלי לרכיב וניתן לשנות אותן או לממש אותן אחרת.

ברכיב זה אין שימוש של החצים למעלה ולמטה למרות שאפשר לממש גם כאן בדומה לרכיב הסלקט ,

אבל ברכיב זה נוסף בדיקה של לחיצה על ESC כדי לסגור את רשימת הבחירה שלנו.

במידה שנרצה לאפשר קבלה של נתונים מסומנים כברירת מחדל מהטופס,

צריך לממש את פונקציה writeValue שתרוץ על המערך נתונים ותסמן את תיבות הסימון המתאימות.

איך מתקשרים עם הרכיב שלנו:

reactive-form

export class ReactiveFormPageComponent implements OnInit {
  multiSelect: IMultiSelect[] = multiSelectData; // SOME DATA
  form:FormGroup;
  constructor(private fb:FormBuilder) { }

  ngOnInit() {
    this.form = this.fb.group({    
     multiSelect:[''],
     multiSelect1:['',Validators.required], 
    });
  }

}
<form [formGroup]="form">
    <div class="area-wrapper">     
        <app-multi-select formControlName="multiSelect" [selectData]="multiSelect" dataId="multi1"></app-multi-select>
        <app-multi-select formControlName="multiSelect1" [selectData]="multiSelect" dataId="multi2"></app-multi-select>
    </div>
</form>

<p>required: {{form.valid}}</p>
<pre>{{ form.value | json }}</pre>

template form

export class TemplateFormPageComponent implements OnInit {
  multiSelect: IMultiSelect[] = multiSelectData; 

  constructor() { }

  ngOnInit() {
  }

}
<form #form="ngForm">
    <div class="area-wrapper">      
        <app-multi-select name="multiSelect" ngModel [selectData]="multiSelect" required dataId="multi3"></app-multi-select>
        <app-multi-select name="multiSelect1" ngModel [selectData]="multiSelect" required dataId="multi4"></app-multi-select>
    </div>
</form>

<p>required: {{form.valid}}</p>
<pre>{{ form.value | json }}</pre>

בהצלחה 🐊

פוסטים קשורים:

תמונת אווירה של מטריה כתומהרכיבים: בניה של רכיב SELECT נגיש באנגולר תמונת אווירה של אופציותרכיבים: בניה של רכיב AUTOCOMPLETE נגיש באנגולר תמונת אווירה של חצים לכל הכיווניםבניה של טופס דינמי על פי מידע מהשרת באנגולר תמונת אווירה של רשימה עם תיבות סימוןרכיבים נגישים: כפתורי רדיו ותיבות סימון מעוצבים
angular אנגולר מדריך אנגולר רכיבים

אודות המחבר

עידן יצחקי להציג את כל הפוסטים של עידן יצחקי


« פוסט קודם
פוסט הבא »

השארת תגובה

ביטול

חיפוש באתר
בחירת העורכים
29 בדצמבר 2023 עידן יצחקי

שדה טקסט עשיר עם תמונות

אתם הולכים להיות מופתעים עד כמה HTML יכול להיות חכם ולבצע משהו כל כך מורכב, שאם אנחנו היינו רוצים ליצור

1 באוקטובר 2021 עידן יצחקי

איך למשוך דינמית favicon של אתרים אחרים ב-JS

בפוסט זה נראה איך אפשר על פי לינקים בדף למשוך את ה-favicon מהדומיין שלהם באופן דינמי, בדיקה של תקינות התמונה

פופולרי
Javascript functions – היכרות עם סוגי פונקציות
Javascript
21 בדצמבר 2024 אין תגובות
Nested routing in angular standalone component
Typescript
15 בנובמבר 2024 אין תגובות
בחרו לפי תגיות
angular blockchain css ethers express front-end fullstack GQL html javascript next js nextjs nodejs react hooks reactjs solidity webgl אנגולר בלוקציין וורדפרס לימודי אנגולר לימודי וורדפרס לימוד ריאקט מדריך front-end מדריך GQL מדריך אנגולר מדריך וורדפרס מדריך חינם react מדריך ריאקט מפתח בלוק מפתח בלוקציין מתכנת front-end מתכנת בלוקציין מתכנת פרונט סולידיטי קורס front end קורס fullstack קורס nextjs קורס אנגולר קורס בלוקציין קורס בלוקציין בחינם קורס סולידיטי קורס ריאקט קורס תכנות קורס תכנות בחינם
סינון על פי קטגוריות
CSS fullstack HTML IIS Javascript nodeJs SEO Typescript אנגולר בלוקציין בניית אתרים וורדפרס חיפוש עבודה כלים נוספים כללי נגישות קורסים ריאקט תלת מימד תקלות ופתרונות
צור קשר
כל הזכויות שמורות לקודקודייל
ליצירת קשר: @ קודקודייל
גלילה לראש העמוד
דילוג לתוכן
פתח סרגל נגישות כלי נגישות

כלי נגישות

  • הגדל טקסטהגדל טקסט
  • הקטן טקסטהקטן טקסט
  • גווני אפורגווני אפור
  • ניגודיות גבוההניגודיות גבוהה
  • ניגודיות הפוכהניגודיות הפוכה
  • רקע בהיררקע בהיר
  • הדגשת קישוריםהדגשת קישורים
  • פונט קריאפונט קריא
  • איפוס איפוס