Accordion 源码分析

178 阅读1分钟

中文名:手风琴

更清晰的格式,欢迎访问语雀 www.yuque.com/uov16w/tq9o…

演示地址 angular.carbondesignsystem.com/?path=/stor…

组件及属性

  • ibm-accordion
    • Input
      • align: start|end = end
      • skeleton: boolean = false;
  • ibm-accordion-item
    • Input
      • title: string| TemplateRef;
      • context: Object | null = null;
      • id = accordion-item-${AccordionItem.accordionItemCount}
      • skeleton = false;
    • Output
      • selected

Tips

ContentChildren 指令

使用 ng-content 指令可以实现 Content Projection 内容投影,类似Vue的slot,插槽。如果存在多个 ng-content,可以通过 select 指定要投射的位置。

@Component({
    selector: 'exe-parent',
    template: `
      <p>Parent Component</p>  
      <ng-content></ng-content>
    `
})

@Component({
  selector: 'my-app',
  template: `
    <h4>Welcome to Angular World</h4>
    <exe-parent>
      <exe-child></exe-child>
    </exe-parent>
  `,
})

ContentChild 是属性装饰器,用来从通过 Content Projection 方式设置的试图中获取匹配的元素。

ContentChildren 属性装饰器,和 ContentChild相比,就是获取匹配的多个元素,返回的结果是一个 QueryList 集合。

那 ContentChildren 和 ViewChildren 有啥区别呢?

  • 在父组件的开始结束标签中间放入的 元素,成为 ContentChildren.
  • 在组件自己的模板中定义的内容,是组件的一部分,称为 ViewChildren.

ContentChild 和 ViewChild 区别?

  • ContentChild 用于获取通过内容投影方式设置到试图中的元素。
  • ViewChild 用于从模板中获取匹配的元素
  • 在父组件的 ngAfterContentInit 生命周期中,才能获取通过 ContentChild 查询的元素
  • 在父组件的 ngAfterViewInit 生命周期中,才能获取通过 ViewChild 查询的元素。

相关文章

ngTemplateOutlet/ngTemplateOutletContext 使用

codesandbox.io/s/strange-b… 实时尝试

@Component({
  selector: '',
  template: `
  <!-- 
    这里即实现了动态展示模板,并且 ng-container 仅仅是站位标签,
    实际dom渲染后并不会有ng-container这一层显示。
    
    <ng-container *ngTemplateOutlet="estimateTemplate" *ngTemplateOutletContext="ctx"></ng-container>
  -->
  <ng-container *ngTemplateOutlet="estimateTemplate; context: ctx"></ng-container>
  
  
  
  <!-- 
    注意,这里的let-xxx,这个xxx是模板中能够使用的变量
    estimate 则是 ts 中定义的ctx的属性值。这个对应关系需要注意
  -->
  <ng-template #estimateTemplate let-lessonsCounter="estimate">
    <div> Approximately {{lessonsCounter}} lessons ... </div>
  </ng-template>
`})
export class AppoComponent {
  total = 10;
  ctx = { estimate: this.total }
}


/*
 * 或者下面的方式也可以
 * 上面是简写,这里是完整写法。
 */
@Component({
  selector: "app-root",
  template: `
    <!-- 
      这里的 $implicit 看着有点怪,不过算是固定写法,
      可以看的到,还可以传入其他参数,如示例的 idx。
    -->
    <ng-container
      [ngTemplateOutlet]="estimateTemplate"
      [ngTemplateOutletContext]="{ $implicit: estimate, idx: 1 }"
    ></ng-container>

    <ng-template #estimateTemplate let-estimate let-position="idx">
      <div>Approximately {{ estimate }} lessons ...</div>
      <div>positoin = {{position}}</div>
    </ng-template>
  `,
  styleUrls: ["./app.component.css"]
})
export class AppComponent {
  total = 10;
  estimate = 20;
}

参考

HostBinding/HostListener

HostBinding 和 HostListener 长得很像,先看一个 HostListener 的 example:

import { Directive, ElementRef, Renderer, HostListener } from '@angular/core';

@Directive({
  selector:'[appChbgcolor]'
})
export class ChangeBgColorDirective {
  constructor(private el: ElementRef, private renderer: Renderer) {
  }
  
  // 这里监听了 mouseover 事件
  // 当鼠标移动到该 host 组件时,执行其中的逻辑
  @HostListener('mouseover') onMouseOver() {
    this.ChangeBgColor('red')
  }
  
  @HostListener('click') onClick() {
    // click event
  }
  
  @HostListener('mouseleave') onMouseLeave() {
    // mouseleave event
  }
  
  ChangeBgColor(color: string) {
    this.renderer.setElementStyle(this.el.nativeElement, 'color', color);
  }
}

// 使用该 directive
`
<!-- 我们所说的 host 元素是 appChbgcolor 所在的元素,也就是 div-->
<div appChbgcolor>
  <h3>{{title}}</h3> 
</div>
`

所以该 example 可以看出 HostListener 指令的作用是处理来自host(托管)元素的事件

再来看一个 HostBinding 的 example:

// 这里使用 border 变量绑定了 host 元素的 border 属性
@HostBinding('style.border') border: string;

@HostListener('mouseover') onMouseOver() {
  // 这里修改 border 变量的值,来达到修改 host 元素 border 属性的目的
  this.border = '5px solid green'
}

所以,可以看出 HostBinding 的作用就是修改 host 元素的属性

参考

import {
  Component,
  Input,
  ContentChildren,
  QueryList,
  AfterContentInit
} from "@angular/core";
import { AccordionItem } from "./accordion-item.component";

/**
* [See demo](../../?path=/story/components-accordion--basic)
*
* <example-url>../../iframe.html?id=components-accordion--basic</example-url>
*/
@Component({
  selector: "ibm-accordion",
  template: `
  <ul class="bx--accordion"
  [class.bx--accordion--end]="align == 'end'"
  [class.bx--accordion--start]="align == 'start'">
  <ng-content></ng-content>
  </ul>
  `
})
export class Accordion implements AfterContentInit {
  @Input() align: "start" | "end" = "end";
  
  // 获取全部的 accordion-item 组件,便于后面统一设置 skeleton 属性
  @ContentChildren(AccordionItem) children: QueryList<AccordionItem>;
  
  protected _skeleton = false;
  
  @Input()
  // 这里使用 _skeleton, set 方式,方便当传入的 skeleton 属性变化时,做其他逻辑,
  // 如本组件的更新子组件的 skeleton 属性
  set skeleton(value: any) {
    this._skeleton = value;
    this.updateChildren();
  }
  
  get skeleton(): any {
    return this._skeleton;
  }
  
  ngAfterContentInit() {
    this.updateChildren();
  }
  
  protected updateChildren() {
    if (this.children) {
      this.children.toArray().forEach(child => child.skeleton = this.skeleton);
    }
  }
}
import {
	Component,
	Input,
	HostBinding,
	Output,
	TemplateRef,
	EventEmitter
} from "@angular/core";

@Component({
	selector: "ibm-accordion-item",
	template: `
		<button
			type="button"
			[attr.aria-expanded]="expanded"
			[attr.aria-controls]="id"
			(click)="toggleExpanded()"
			class="bx--accordion__heading">
			<svg ibmIcon="chevron--right" size="16" class="bx--accordion__arrow"></svg>
			<p *ngIf="!isTemplate(title)"
				class="bx--accordion__title"
				[ngClass]="{
					'bx--skeleton__text': skeleton
				}">
				{{!skeleton ? title : null}}
			</p>
      <!-- 这个写法已经在 tips 中说明 -->
			<ng-template
				*ngIf="isTemplate(title)"
				[ngTemplateOutlet]="title"
				[ngTemplateOutletContext]="context">
			</ng-template>
		</button>
		<div [id]="id" class="bx--accordion__content">
			<ng-content *ngIf="!skeleton; else skeletonTemplate"></ng-content>
			<ng-template #skeletonTemplate>
				<p class="bx--skeleton__text" style="width: 90%"></p>
				<p class="bx--skeleton__text" style="width: 80%"></p>
				<p class="bx--skeleton__text" style="width: 95%"></p>
			</ng-template>
		</div>
	`
})
export class AccordionItem {
  // 这个静态属性很有意思,用于记录自己是第几个
  // 如果一个页面引入了多组 Accordion 组件,每个下有几个 AccordionItem, 那这个值是怎么样的呢,待测试。
	static accordionItemCount = 0;
  // 表示同时支持 string 和 template 传入,方便自定义
	@Input() title: string | TemplateRef<any>;
  // 此处的 context 表示当 title 为 templateRef 时,设置的上下文
	@Input() context: Object | null = null;
  // 这个id通过这种方式实现很简单,不需要引入比如 uuid 之类的库
	@Input() id = `accordion-item-${AccordionItem.accordionItemCount}`;
	@Input() skeleton = false;
	@Output() selected = new EventEmitter();

  // 通过上面 HostBinding 的介绍,这里的写法也很清晰
  // 给 host 元素设置该类名
	@HostBinding("class.bx--accordion__item") itemClass = true;
  
  // 给 host 元素设置类名,默认是 false,通过下面的逻辑来变更
  // 同时这里的 expanded 是 @Input,所以是支持作为 props 传入到组件中
	@HostBinding("class.bx--accordion__item--active") @Input() expanded = false;
  // host 元素的 style display 设置为 list-item
	@HostBinding("style.display") itemType = "list-item";
  // host 元素的 role 属性设置为 heading
	@HostBinding("attr.role") role = "heading";
  // host 元素的 aria-level 属性设置 为 3,同时支持作为 props 传入
	@HostBinding("attr.aria-level") @Input() ariaLevel = 3;

	constructor() {
    // 每次实例化该组件时,默认加 1
		AccordionItem.accordionItemCount++;
	}

	public toggleExpanded() {
		if (!this.skeleton) {
			this.expanded = !this.expanded;
			this.selected.emit({id: this.id, expanded: this.expanded});
		}
	}

	public isTemplate(value) {
    // 这个判断能够知道 title props 是传入的字符串还是 template,很有参考意义
		return value instanceof TemplateRef;
	}
}

到这里我们对 accordion 组件也有了一定的了解了。

朋友你好,如果你对我的文章感兴趣,欢迎关注我的 Github (github.com/llccing),或者掘金 (juejin.cn/user/322782…),或者语雀 (www.yuque.com/uov16w),感谢你的支持!