Giter Club home page Giter Club logo

ui's Introduction

Alauda UI

Internal Angular UI framework for Alauda Frontend Team.

TOC

Online Demo

Storybook Demo

Getting Started

Install

# npm
npm i @alauda/ui

# yarn
yarn add @alauda/ui

and also need to confirm the peer dependencies have been installed

yarn add dayjs @angular/cdk

Project Config

// tsconfig.json
{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "skipLibCheck": true,
    ...
  },
  ...
}

Usage

import { ButtonModule } from '@alauda/ui';

@NgModule({
  imports: [ButtonModule],
})
export class AppModule {}

Development

git clone https://github.com/alauda/alauda-ui.git
cd alauda-ui
yarn install
yarn start

开发环境基于 Storybook 运行,查看 文档

Test

yarn test

or

yarn test:watch

Build Storybook

yarn storybook:build

Build Library

yarn build

Incremental Builds

Develop and debug UI component libraries quickly and efficiently by incremental builds

Parameter

yarn build:watch

Also can copy a dist to another project to debug

yarn build:watch <project_path>

In this way, after every incremental build completed, dist will be copied to node_modules which in specified project

Config File

In order to incremental build dist to your project directly instead of adding parameter to specify project path every time, can use your own ng-package.json by

npm run debug

Edit you own build config by adding a new file called ng-package.debug.json, like

// ng-package.debug.json
{
  "$schema": "./node_modules/ng-packagr/ng-package.schema.json",
  "dest": "/home/alauda/projects/<target_project_path>/node_modules/@alauda/ui",
  "lib": {
    "entryFile": "./src/index.ts"
  }
}

Read More

LICENCE

MIT © Alauda

ui's People

Contributors

2eron avatar actions-user avatar alaudabot avatar brianshencc avatar dependabot[bot] avatar edisonsu768 avatar fengtianze avatar github-actions[bot] avatar igauch avatar jounqin avatar jyhan-figer avatar kkxiaoa avatar liyouzhi666 avatar mario-mui avatar tunblr avatar wszgrcy avatar yangxiaolang avatar yinshuxun avatar zangguodong avatar zchanges avatar zhangmq avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ui's Issues

scroll-table 行高相比 table 少1px, 不满足与上下 border 相加最小 60px 的高度

image

table的区别是
table 的 border 是在 table-row 上设置的
scroll-table 横向滚动因为可能 table-row 被隐藏部分不算在 row 元素上,导致row部分没有背景和边框,所以设置了交叉轴stretch, 并把border设置在了cell 上,导致了 1px 的缺失,因为 border 的 1px 算在 table-rowheight 里了
image
需要对 scroll-table 的 row height 做特殊处理

优化 drawer

  1. fix:使用 service 关闭 drawer 没有动效
  2. 使用组件方式不会先创建 overlay,也不会在打开时又创建了一个 overlay

重制table组件

aui-table历史问题导致功能受限,因此重制一个aui-power-table

【 drawer 】在打开抽屉后手动触发一次变更检测

理由:7.3.0 之前的 open 有手动变更检测;如果不加上,容易造成一些超出预期的问题,例如使用 viewchild 查找 drawercontent 里的某个元素或组件,因为 drawercontent 是异步的,所以初始时这个 viewchild 是undefined

【select】增加readonly支持

select组件支持readonly,单选和多选的readonly样式如下:
image
image

实现思路:

  1. 在 CommonFormControl 中提升 readonly 相对 disabled 的权重,readonly 为 true 时,disabled 就一定为true。同时在 scss 中也将readonly 规则放在 disabled 后
  2. 原 baseSelect 中的 inputReadonly 其实是在表示 input 为 非活动的状态,所以这次改为 inaction,因不涉及逻辑层,所以只调整样式选择器即可

【drawer】优化重构

历史 drawer

service

  1. service 打开的还是 DrawerComponent

component

  1. 会立即创建一个 overlay 出来,其实就是实例化,只是通过控制样式来达到显示隐藏

问题

component

  1. 未打开前不应该先实例化

service

  1. 打开会有两个 overlay 实例
  2. 关闭时会立即发出 close 事件并销毁 service 创建的 overlay,所以关闭动画就看不到了

现状

  1. 使用 service 时关闭会销毁;使用组件方式时,因为是跟随组件的生命周期,所以关闭并不会销毁实例
  2. 使用服务多次调用 open 会先销毁上一个再创建出一个新的,但对于组件方式因为 visible 没变,所以并没有任何的改变。

改进

目标

  1. 未打开前不应该先实例化
  2. service 方式不应该存在两个 overlay
  3. 关闭时应该有动效

设计

  1. service 是创建 drawer 的唯一实现,component 只是 service 的一个包装
  2. 原来 DrawerComponent 改成内置的抽屉组件,用来定义抽屉组件的渲染
  3. 关闭事件分为关闭前和完成关闭两个事件,动画完成后再发出完成关闭事件
  4. 新增 disposeWhenHide 配置,用来区分实现 现状1 里组件和服务的差异,并暴露给用户,使其具有选择权。
  5. open 返回值由 DrawerComponent 改成一个新的 DrawerRef,因为 DrawerComponent 已经是一个内置的渲染定义了,不适合作为对外暴露的对象,也不包含需要对外暴露的能力。
  6. 新增 updateOption 方法,用于直接更新抽屉内容,而不是销毁重建(实现的有问题,需要改一下)

变更的影响

  1. 之前组件是唯一的实现,服务是组件的又一层包装;现在改成由内置组件定义抽屉渲染,服务作为唯一的实现,组件只是服务的一个包装。
    • 执行的先后顺序变了,配置的承载主体变了

验证点

添加单测,把下面完整的验证点都写一下

Jest 单测 ReferenceError: Cannot access 'AutocompleteComponent' before initialization

image
Jest 执行单元测试 yarn test underlord 会报错

ReferenceError: Cannot access 'AutocompleteComponent' before initialization

根据堆栈信息查看到这部分代码

image

产物的类声明的顺序产生是: 编译 AutoCompleteComponent 组件时,该组件依赖了 SuggestionComponent 组件,所以要优先处理 SuggestionComponent。但是 SuggestionComponent 也注入了 AutoCompleteComponent ,此时 AutoCompleteComponent 还未声明,导致了该问题,也就是循环依赖产生的问题。

  constructor(
    private readonly cdr: ChangeDetectorRef,
    private readonly autocomplete: AutocompleteComponent,
  ) {}

所以在 SuggestionComponent 注入时,需要使用 forwardRef 来让注入的组建 token 生成在类声明之后

  constructor(
    private readonly cdr: ChangeDetectorRef,
    @Inject(forwardRef(() => AutocompleteComponent))
    private readonly autocomplete: AutocompleteComponent,
  ) {}

此外,可以使用 npx madge --circular --extensions ts ./ 来检查项目中的循环依赖问题
image
可以看到forwardRef 只是处理了循环依赖产生的死区问题,并未解决循环依赖本身。循环依赖是产生这种问题的必要条件而不是充分条件,其他也具备循环依赖的地方,并没有在组件类中产生注入,所以没有出现此类报错。

Tags input加校验

现有问题:
目前aui-tags-input无法支持对输入数据进行校验,判断是否有效后再插入。导致的问题就是,如果输入不合法的数据,必须在tag生成后,删除,这既有一个闪动的效果,也无法很好的展示错误原因(只能拉一个notificationService),而且用户也不好调整错误数据
实现目标效果:
image

实现难点:
由于aui-tags-input本身作为ControlValueAccessor,暴露给FormControl,NgModel等指令的数据是生成后的数组形式,也就是说,FormControl,NgModel等的所有相关校验器,无论是在模板上指令添加:[pattern]='xxx',或ts添加: this.fb.control('',Validator.requried),都应该是针对数组形式的数据。问题是,数组所展示的数据,是在输入已经完成后,再插入到数组中的,此时想对输入数据做校验,已经太晚了。

实现思路:
保留用户对aui-tags-input数组形式数据的校验器,对于输入数据的校验,会提供单独的输入变量。两个输入分别为
inputValidatorFn,inputAsyncValidatorFn
来分别解决对输入数据的校验与异步校验的问题
可传入单个校验器,或者数组校验器,实现时会将数组形式的多个校验器合并。这些校验器会传递给一个内置的FormControl变量,在每次将要生成tag时,先根据输入的value,对该内部FormControl进行校验,判断是否应该生成,以及什么时候生成tag。如果校验为invalid,会给外层的 NgControl setError一个 {inputError: {...}},来确保不与 aui-tags-input 自身的error冲突

[Drawer] Drawer 组件 block 页面滚动事件且会在 resize 后失效

需要的改动

  1. 根据 mask 输入来调整 overlayRef 的 scrollStrategy
  2. Drawer 组件内部监听 resize 事件激活滚动策略

问题描述

按照设计规范,应当只在存在遮罩层时 block 底部内容区的交互
企业微信截图_cb0110a7-ca93-4dea-b59c-fdb684c07125(1)

当前在无遮罩层时 click 操作是正常的。
若底部内容区可以滚动,则只要打开 Drawer ,无论是否有遮罩层,都无法滚动页面

2023-10-09.16-56-03.mp4

原因是 Drawer 默认的 scrollStrategy 固定为了阻止页面滚动

  private getOverlayConfig(): OverlayConfig {
    return new OverlayConfig({
      panelClass: DRAWER_OVERLAY_CLASS,
      positionStrategy: this.overlay.position().global(),
      scrollStrategy: this.overlay.scrollStrategies.block(),
    });
  }

image

应当根据 mask 输入来选择不同的 scrollStrategy 策略

此外,还有一个 angular/cdk 的已知问题,即 scrollStrategy 为阻止页面滚动时,如果打开抽屉后,缩放了窗口触发了 window:resize 事件,会导致页面可以滚动

2023-10-09.17-06-30.mp4

原因是,抽屉的滚动策略生效是在抽屉打开时,若打开时就不需要滚动,则阻止页面滚动不会生效,详情见 angular/components#10841

可能需要抽屉内部监听 resize 事件来重新激活滚动策略

【table】支持拖拽调整列宽

新增一个指令能通过拖拽调整列宽

实现思路:

  1. 新增拖拽能力指令,通过扩展该指令实现 table 的 resizable 指令

[Dialog] 浏览器前进后退行为不会使 Dialog 关闭

现状

在打开对话框的时候依然可以使用浏览器的前进后退功能切换页面,该行为也不会导致对话框的关闭

2023-10-18.16-16-25.mp4

预想处理方案

1. 对话框阻断浏览器的前进后退行为(是否需要提示,怎么提示
2. 对话框监听路由事件,切换路由时关闭自身(是否存在跨页面生命周期的对话框)

跟设计讨论,对话框阻塞浏览器行为不太合理,且对话框组件延用表单的阻断性提示并不合理,采用 路由切换对话框自己关闭 实现

bug: drawer bugs

  1. 使用aui-drawer组件默认值不生效,导致没有关闭按钮。
    原因是因为传到service的aui-drawer组件的配置为undefined,在进行展开合并操作后默认值又被undefined覆盖了,问题代码:

    this.options = {
      ...(DEFAULT_OPTIONS as DrawerOptions<T, C>),
      ...options,
    };
    

    问题没有被发现的原因是:
    aui的demo没有问题,aui-drawer组件的input都是不存在的字段,而不是存在但值是undefined

  2. 支持 contentParams 以 $implicit 作为 template 的 context,同时继续支持指定key的方式

date-picker 最大最小时间禁用改造

背景

当前 AUI 的 date-picker 组件支持通过 minDate, maxDate,disabledDateFndisabledTimeFn 来禁用日期时间。
但是在支持时间选择时会存在问题:

  1. 当未选择时间时,选择日期会默认选择 当前时间,但是 当前时间 未必是 有效时间
  2. 当未选择时间时,用于校验最大最小日期时间的 标准时间,也会是 当前时间;这导致了默认面板上当 当前时间 小于 最小时间 时,最小日期 自动被禁用了,而当 当前时间 大于 最大时间 时, 最大日期 也是被自动禁用了,这是错误的。

Reproduction: https://stackblitz.com/edit/stackblitz-starters-tmxl9m?file=src%2Fmain.ts

结论

1. 禁用逻辑修复

date-picker 的日期面板在禁用判断时,不比较时间。即

无论当前时间以及最大最小日期中设置的最大最小时间是多少,都不会禁用 [ 最小日期,最大日期 ]

选择日期后,自动填充的时间仍然使用 当前时间,即使 当前时间 并不在允许的时间范围内,需要手动重新选择时间,out_of_range 的错误由各自的业务进行表单验证抛出

2. 新增默认行为

选择日期后,会根据设置的 minDatemaxDate 的值得到最大最小日期的最大最小时间,当选择这些日期时,time-picker 会对无效时间进行禁用

以下为初始改造方案,未通过 Review

未通过 review 的原因是

  1. 通用组件不要有太多隐含的改变 value 的规则,约束有效性尽量交给表单验证
  2. 此刻 是一个共识,即当前时间; 为了有效性而改变共识,会造成理解负担,见1

改进目标

  1. 时间未选择时,比对 最大最小时间 时应该使用 当日的最大最小时间 而非 当前时间
  2. 选择日期时,应该选择 距离当前时间最近的有效时间
  3. 选择时间后,若时间小于 最小时间,禁用 最小日期; 若时间大于 最大时间,禁用 最大日期
  4. 同样的,选择时分秒时,若 选择了 边缘值 导致 的值变为无效,应该 fallback 到最近的有效值

需要改进的组件有 date-pickerrange-picker。改进后只使用 minDatemaxDate 就应该能适用于大部分场景,特殊情况使用 disabledDateFndisabledTimeFn 来处理。

time-picker 暂时不需要处理,因为时分秒的禁用本身就直接依赖 disabledTimeFn 执行结果返回的禁用数组。

改造内容

  1. selectedTime 数据流改造
    date-pickerselectedTime 接受外部参数传入,并向下传递给内部默认的 picker-panel 用于时间的校验。

    接受外部参数传入是因为 footerTemplate 是可以自定义的。

    <aui-picker-panel
        [style.margin]="'16px 0'"
        [anchor]="anchor"
        [navRange]="navRange"
        [disabledDate]="disabledDate"
        [weekStartDay]="weekStartDay"
        [minDate]="minDate"
        [maxDate]="maxDate"
        [selectedTime]="selectedTime"
        [type]="type"
        [matchDates]="[selectedDate]"
        (select)="panelValueChange($event)"
    ></aui-picker-panel>
  2. time-picker 输入输出增加 validResult 转换
    函数式依次处理传入的 Dayjs 的时分秒,来得到最终的有效时间,处理函数见 src/time-picker/util.ts

    在相关的 time-pick 的组件和 Panel 上的 value 的 write 和 emit 时增加 validResult 过滤,传入与传出的值都是有效的

    private validResult = validResult({
      hours: this.disableHours,
      minutes: this.disableMinutes,
      seconds: this.disableSeconds,
    });
    
    override writeValue(value: TimePickerDataLike) {
      if (!value) {
        return this.setValue(null);
      }
      let result: Dayjs;
      if (isTimePickerModel(value)) {
        result = updateDateByTimeModel(dayjs(), value);
      } else {
        result = dayjs(value);
        this.submit(false, result);
      }
      const validResult = this.validResult(result);
      this.setValue(validResult);
      super.writeValue(validResult);
    }
    
  3. picker-panel 时间禁用函数的逻辑修改

    // picker-panel.ts
    get disabledDateFn() {
        return composeDisabledDateFn(
            date =>
              this.minDate &&
              date
                // 比较最小时间的如果未选择时间,要选择一天的最晚时间
                .set('hour', this.selectedTime?.hour || 23)
                .set('minute', this.selectedTime?.minute || 59)
                .set('second', this.selectedTime?.second || 59)
                .isBefore(this.minDate),
            date =>
              this.maxDate &&
              date
                // 比较最大时间的如果未选择时间,要选择一天的最早时间
                .set('hour', this.selectedTime?.hour || 0)
                .set('minute', this.selectedTime?.minute || 0)
                .set('second', this.selectedTime?.second || 0)
                .isAfter(this.maxDate),
            this.disabledDate,
        );
    }
  4. date-picker 默认的禁用时间函数改造
    date-picker 也支持时间选项,所以也能接收禁用时间的函数传递给 time-picker,但是并没有根据自身 minDatemaxDate 去做默认禁用,所以增加一个默认的禁用时间函数。

    这里感觉有点问题,是不是还需要提供一个 minTimemaxTime,不然用户在使用 minDatemaxDate 的时候可能未考虑 时间 部分

     getDisabledTimeFn(
       selectedDate: Dayjs,
       type: keyof ReturnType<DisabledTimeFn>,
     ) {
       if (selectedDate !== this._cacheSelectedDate) {
         this._cacheDisabledTimeFn = combineDisabledTimeFn(
           this._disabledTimeFn.bind(this),
           this.disabledTime,
         )(selectedDate);
         this._cacheSelectedDate = selectedDate;
       }
       return this._cacheDisabledTimeFn?.[type];
     }
     
     private _disabledTimeFn(selectedDate: ConfigType) {
       const returnFnMap: Record<
         keyof ReturnType<DisabledTimeFn>,
         () => number[]
       > = {
         hours: () => [],
         minutes: () => [],
         seconds: () => [],
       };
    
       if (
         selectedDate &&
         this.minDate &&
         (selectedDate as Dayjs)?.isSame(this.minDate, 'day')
       ) {
         returnFnMap.hours = () =>
           HOUR_ITEMS.filter(item => item < this.minDate.hour());
         returnFnMap.minutes = (hour?: number) => {
           if (hour === this.minDate.hour()) {
             return MINUTE_ITEMS.filter(item => item < this.minDate.minute());
           }
           return [];
         };
         returnFnMap.seconds = (hour?: number, minute?: number) => {
           if (hour === this.minDate.hour() && minute === this.minDate.minute()) {
             return SECOND_ITEMS.filter(item => item < this.minDate.second());
           }
           return [];
         };
       }
    
       if (
         selectedDate &&
         this.maxDate &&
         (selectedDate as Dayjs)?.isSame(this.maxDate, 'day')
       ) {
         returnFnMap.hours = () =>
           HOUR_ITEMS.filter(item => item > this.maxDate.hour());
         returnFnMap.minutes = (hour?: number) => {
           if (hour === this.maxDate.hour()) {
             return MINUTE_ITEMS.filter(item => item > this.maxDate.minute());
           }
           return [];
         };
         returnFnMap.seconds = (hour?: number, minute?: number) => {
           if (hour === this.maxDate.hour() && minute === this.maxDate.minute()) {
             return SECOND_ITEMS.filter(item => item > this.maxDate.second());
           }
           return [];
         };
       }
    
       return returnFnMap;
     }

    并使其可以与传入的 disabledTime 组合取禁用时间的并集,用 combineDisabledTimeFn 将多个函数的结果组合

     function combineDisabledTimeFn(
      ...disabledFnList: DisabledTimeFn[]
     ): DisabledTimeFn {
       return (date?: ConfigType) => ({
         hours: () =>
           Array.from(
             new Set(
               disabledFnList
                 .map(fn => fn(date)?.hours?.() || [])
                 .reduce((prev, cur) => [...prev, ...cur], []),
             ),
           ),
         minutes: (hour?: number) =>
           Array.from(
             new Set(
               disabledFnList
                 .map(fn => fn(date)?.minutes?.(hour) || [])
                 .reduce((prev, cur) => [...prev, ...cur], []),
             ),
           ),
         seconds: (hour?: number, minute?: number) =>
           Array.from(
             new Set(
               disabledFnList
                 .map(fn => fn(date)?.seconds?.(hour, minute) || [])
                 .reduce((prev, cur) => [...prev, ...cur], []),
             ),
           ),
       });
     }

日期选择样式优化

日期选择面板的按钮不应该再有padding,已经固定宽度且做了居中设定,所以padding是无关紧要的,并且如果字体变宽等会造成...现象,不设定padding以留足因font-family变化而导致文本宽度变化的横向空间
image

新设计变量系统意见征求

设计稿色板命名和代码中命名请看链接:新色板PR

计划将全部色值和少量全局通用的尺寸值用 CSS 变量定义到页面全局,以便支持深浅色切换和自定义主题。在替换 UI 的过程中逐步移除以前定义的 SCSS 变量,如 color: $color-primary, font-size: $font-size-regular 等,改用自定义的 SCSS 函数获取 CSS 变量,如 color: getColor(primary), font-size: getSize(font-size, m)

会产生的影响:

  1. scss 代码中无法再使用 scss 计算。尺寸值可以改成 css calc 运行时计算,但色值相关的计算(mix,添加 alpha 通道等)无法运行时计算,因此所有的颜色都必须让设计给出固定的值。

待定的问题:

  • 开发时可以很简单的通过色板命名看出代码中如何取值,如色斑命名为 N1,对应代码为 getColor(neutral, 1)。但是很难看出getColor(neutral, 1) 对应的使用场景是什么。是否需要添加按使用场景命名的全局变量如 $color-text-main: getColor(neutral, 1)
  • 单个组件内用到样式值是否需要通过全局 css 变量的方式支持用户自定义

时间组件

时间组件

在现有业务中存在一些选择时间,不同业务中操作行为及ui上都存在不一致的情况,并且在现有时间组件臃肿操作不方便,因此需要统一产品中时间组件

业务中已存在的组件

  1. 选择年月
    image
  2. 选择时间
    image
  3. 选择日期
    image

需产出组件

  • year-picker
  • month-picker

  • time-picker

  • date-picker
  • range-picker

  • week-picker

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.