angular 表格 动态加载列_在angular中编写可复用代码(二):用指令实现列表的瀑布流加载...

8a9f29aa5f043c6a440cef3b1f947218.png
https://zhuanlan.zhihu.com/p/165118088​zhuanlan.zhihu.com
zhihu-card-default.svg

接上篇,关于请求并处理分页数据的纯逻辑部分,我们已经通过服务实现。现在需要关注的是页面交互部分:怎样实现用户滚动到列表底部时,自动加载下一页数据呢?我们首先想到的就是指令

@Directive({
  selector: '[appScrollLoading]',
})
export class ScrollLoadingDirective implements OnInit, OnDestroy {
  
  loadingDom: HTMLElement;
  loadingDomRef: ComponentRef<any>;
  loadingState$: Subject<state>; 
  end$ = new Subject()

  constructor(
    private el: ElementRef,
    private cfr: ComponentFactoryResolver,
    private appRef: ApplicationRef,
    private injector: Injector,
    @Optional() @SkipSelf() private listData: ListDataService<unknown>
  ) {    
    this.loadingState$  = this.listData?.loadingState$    
  }
}

在这里我需要明确一下,我们编写的指令,跟上一节我们写的服务是高度关联的(见上述代码)。也就是说,我们需要将PagingDataService服务注入到指令中来。毕竟要请求下一页数据,不可避免地要调用服务中nextPage方法,并且指令还得知道http请求何时开始,何时结束,这样才能在页面底部展示相应的文字提示,而这点也必须通过访问服务的loadingState$对象来实现。

这也是我为什么会强调,我们要在渲染分页数据的组件里注册此服务。毕竟我们的指令也是用在该组件上的。这样一来,服务对于组件以及组件内部使用的指令来说就是单例的,而且不会影响到其他同样需要加载分页数据的组件,因为在别的组件那里,PagingDataService又会单独生成一个服务实例。

这也是angular依赖注入的强大之处,由于注入器树的设计,再配合rxjs的响应式,我们想实现一个局部的状态管理方案真的是轻而易举。简直是香格里拉的香!

理论部分讲完,我们来分析具体需求。首先,页面滚动是一个scroll事件,毫无疑问,我们需要在指令中监听该事件,并且等到页面滚动到底部时,再执行对应的方法。下面是代码:

bindScrollEvent() {        
    fromEvent(this.hostEl,'scroll')
      .pipe(
        debounceTime(250),        
        takeUntil(this.end$)
      )
      .subscribe( _ => {        
        const topIns = this.hostEl.scrollTop
        const bottomIns = this.hostEl.scrollHeight - topIns - this.hostEl.offsetHeight   
        if(bottomIns < 15) {  
          this.listData.nextPage()
        } 
      })    
  }

我们定义了一个bindScrollEvent方法,并再ngOninit钩子内直接调用。可以注意到,我并没有用angular内置的hostListener的事件监听,而用了rxjs内置的dom事件捕获函数。因为rxjs的fromEvent可以直接将事件转化为流,这样我们就能通过操作符debounceTime实现滚动事件的防抖功能。这样比angular自带的方法可方便多了(同时不要忘记需要new一个Subject用来取消事件流的订阅)。

此外,还用到了angular的ElementRef来获取指令的宿主元素:

get hostEl() {
    return this.el.nativeElement
 } 

通过监听宿主元素hostEl的滚动事件,我们计算出作为父容器的hostEl的底部与n内部整个文档内容底部的距离,在小于15px时,我们就认为列表即将滚动至容器底部(提醒:作为父容器的hostEl必须是可滚动的,这个需要我们自己把css属性设置好)。这时候,我们便可调用pagingDataService的nextPage()方法。pagingDataService调用该方法后,拿到下一页数据并发布出去,然后被组件订阅到,最后就会自然而然地渲染到页面上。是不是很舒爽?

做到这里,其实该指令的任务基本完成了。当然,还剩一个小任务,就是加载时需要在页面底部给一个文字提示。尤其是网络慢时,用户滚动到底部,看到“正在加载更多”文字提示,对用户的反馈就很友好。

回顾上一节我们知道,pagingDataService有一个Observable对象loadingState$,现在终于派上用场,我们只需要在指令内部监听他就可以了。

listenLoading() {        
    this.pagingDataService.loadingState$
      .pipe(        
        distinctUntilChanged(),        
        takeUntil(this.end$)
      )
      .subscribe(state => {
        switch(state) {
          default:
          case 'success':
            this.loadingDomRef.instance.text = '';
          break;
          case 'end':
            this.loadingDomRef.instance.text = '- 已经到底啦 -';  
          break;          
          case 'error':
            this.loadingDomRef.instance.text = '加载失败';
          break;
          case 'pagePending':
            this.loadingDomRef.instance.text = '加载中...';
          break;
        }
      })    
  }

可以看到,无非就是在不同的加载状态给不同的文字内容(end表示没有更多数据)。当然,你肯定会问这个loadingDomRef是个什么鬼。这里,我们用到了动态组件,此动态组件的模板内容就是一段文本,没有其他:

@Component({
  template:`
    <div class="tip-container">
      <span>{{text}}</span>
    </div>    
  `,
  styles:[`
    .tip-container{      
      margin-top: 15px;
      text-align: center;
      padding: 5px 0;
      min-height: 15px;
      color: #999;
    }
  `]  
})
class LoadingBox {
  text: string ;
}

至于指令怎么将该组件动态插入到宿主元素,代码如下:

insertLoadingDom() {
    const factory       = this.cfr.resolveComponentFactory(LoadingBox)
    this.loadingDom     = document.createElement('loading')
    this.loadingDomRef  = factory.create(this.injector,[],this.loadingDom)   
    // 在第一次加载数据成功后,再将动态组件插入
    this.loadingState$
    .pipe(
      filter( state => ['success','end'].indexOf(state) > -1),
      take(1)
    ).subscribe( _ => {
      this.appRef.attachView(this.loadingDomRef.hostView)
      this.hostEl.appendChild(this.loadingDom)
    })
  }

其中crf为angular内部带有组件工厂函数的服务,可以调用resolveComponentFactory方法传入一个组件类,返回一个组件工厂对象,该对象调用create方法,又会返回刚才传入的组件类的组件实例。拿到这个组件实例,我们就能把他插入到指令的宿主元素中了。

具体的操作可以参看angular官网关于动态组件和自定义元素这两节,讲得其实比较详细了。这两块内容对于实现比较复杂的交互功能来讲还是很重要的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值