Abstract Form--动态生成和修改module

本文详细介绍了如何使用Erlang语言动态生成和修改模块,包括扫描源代码、解析生成抽象形式、编译以及加载对象码等关键步骤。通过实例演示,展示了如何将新增函数的源代码文本转换为可执行的二进制对象码,并最终加载到Erlang虚拟机中使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前面,我们简单描述了Abstract Form的基本组成。现在,我们来看看如何利用Abstract Form动态生成和修改module。
在介绍Erlang Abstract Form--生成和获取,已经提到过,要获得Abstract Form有两种方法,一种读取beam文件中的debug_info,另一种方法就是直接解析源代码。


提供源代码文本
修改一个module最有用的功能是增加新的函数。我们从beam文件可以获取现有模块的Abstract Form,但是如果需要动态增加方法,最容易想到的就是提供函数的源代码文本。
解析源代码通常需要两个工具,即扫描器和解析器。Erlang提供的基本扫描器是erl_scan,解析器为erl_parse。我们看看它们的文档。

    MODULE
    erl_scan
    MODULE SUMMARY
    The Erlang Token Scanner
    DESCRIPTION

    This module contains functions for tokenizing characters into Erlang tokens.
    EXPORTS

    string(CharList,StartLine]) -> {ok, Tokens, EndLine} | Error
    string(CharList) -> {ok, Tokens, EndLine} | Error

    Types:

    CharList = string()
    StartLine = EndLine = Line = integer()
    Tokens = [{atom(),Line}|{atom(),Line,term()}]
    Error = {error, ErrorInfo, EndLine}

    Takes the list of characters CharList and tries to scan (tokenize) them. Returns {ok, Tokens, EndLine}, where Tokens are the Erlang tokens from CharList. EndLine is the last line where a token was found.

    StartLine indicates the initial line when scanning starts. string/1 is equivalent to string(CharList,1).

    {error, ErrorInfo, EndLine} is returned if an error occurs. EndLine indicates where the error occurred.


erl_scan:string方法扫描字符串文本,如果没有发生错误,则在结果tuple中返回所有的token,不然返回错误的行号。
Eshell V5.5 (abort with ^G)
1> c(simplest, [debug_info]).
{ok,simplest}
2> {ok, Tokens, EndLine} = erl_scan:string("test() -> ok.").
{ok,[{atom,1,test},{'(',1},{')',1},{'->',1},{atom,1,ok},{dot,1}],1}
3>

我们传入一个最简单函数test,erl_scan:string扫描的结果返回Tokens,其中包含5个token。分别是atom类型的函数名test、左括号、右括号、函数头的结束->、atom类型的atom ok,以及最后结束的dot。

有了这个 Tokens,我们可以用erl_parse解析生成Abstract Form。erl_parse的文档中说:

    MODULE
    erl_parse
    MODULE SUMMARY
    The Erlang Parser
    DESCRIPTION

    This module is the basic Erlang parser which converts tokens into the abstract form of either forms (i.e., top-level constructs), expressions, or terms. The Abstract Format is described in the ERTS User's Guide. Note that a token list must end with the dot token in order to be acceptable to the parse functions (see erl_scan).
    EXPORTS

    parse_form(Tokens) -> {ok, AbsForm} | {error, ErrorInfo}

    Types:

    Tokens = [Token]
    Token = {Tag,Line} | {Tag,Line,term()}
    Tag = atom()
    AbsForm = term()
    ErrorInfo = see section Error Information below.

    This function parses Tokens as if it were a form. It returns:

    {ok, AbsForm}
        The parsing was successful. AbsForm is the abstract form of the parsed form.
    {error, ErrorInfo}
        An error occurred.


erl_parse可以解析很多种token,包括表达式、term、Form等等,我们需要的是完全解析Form的函数parse_form。同样,如果解析成功,那么返回的tuple中将包含tokens代表的Abstract Form,不然返回语法错误信息。

3> erl_parse:parse_form(Tokens).
{ok,{function,1,test,0,[{clause,1,[],[],[{atom,1,ok}]}]}}

我们可以看到,返回结果中包含了函数的Abtract Form。


编译Abstract Form
有了Abstract Form以后,我们就可以编译,并加载它。

compile模块的文档中说:

    forms(Forms)

    Is the same as forms(File, [verbose,report_errors,report_warnings]).

    forms(Forms, Options) -> CompRet

    Types:

    Forms = [Form]
    CompRet = BinRet | ErrRet
    BinRet = {ok,ModuleName,BinaryOrCode} | {ok,ModuleName,BinaryOrCode,Warnings}
    BinaryOrCode = binary() | term() ErrRet = error | {error,Errors,Warnings}

    Analogous to file/1, but takes a list of forms (in the Erlang abstract format representation) as first argument. The option binary is implicit; i.e., no object code file is produced. Options that would ordinarily produce a listing file, such as 'E', will instead cause the internal format for that compiler pass (an Erlang term; usually not a binary) to be returned instead of a binary.

compile:forms这个函数取要编译的Form作为参数,把它编译成可以被虚拟机执行的二进制对象码数据。它和compile:file基本一样,只不过提供的是Form参数,而compile:file是需要编译的文件名。

实际上,compile:file这个方法我们一直都在用。erl的shell中,输入c(File, options)就是在编译文件:

    c(File) -> {ok, Module} | error
    c(File, Options) -> {ok, Module} | error

    Types:

    File = Filename | Module
    Filename = string() | atom()
    Options = [Opt] -- see compile:file/2
    Module = atom()

    c/1,2 compiles and then purges and loads the code for a file. Options defaults to []. Compilation is equivalent to:

    compile:file(File, Options ++ [report_errors, report_warnings])

    Note that purging the code means that any processes lingering in old code for the module are killed without warning. See code/3 for more information.

c调用compile的file方法编译.erl文件,然后从内存中移去原先存在的代码,然后加载新的代码。

要编译Abstract Form,我们必须提供整个module完整的Form。因此,我们需要提供module属性、export属性等等。


1> c(simplest,[debug_info]).
{ok,simplest}
2> {ok, Tokens, EndLine} = erl_scan:string("test() -> ok.").
{ok,[{atom,1,test},{'(',1},{')',1},{'->',1},{atom,1,ok},{dot,1}],1}
3> {ok, Forms} = erl_parse:parse_form(Tokens).
{ok,{function,1,test,0,[{clause,1,[],[],[{atom,1,ok}]}]}}
4>
4> NewForms = [{attribute, 1, module, simplest},{attribute, 2, export, [{test,0}]}, Forms].
[{attribute,1,module,simplest},
{attribute,2,export,[{test,0}]},
{function,1,test,0,[{clause,1,[],[],[{atom,1,ok}]}]}]
5> compile:forms(NewForms).
{ok,simplest,
<<70,79,82,49,0,0,1,188,66,69,65,77,65,116,111,109,0,0,0,55,0,0,0,6,7,...>>}

加载新编译的对象码
阅读上面文档的另外一个好处就是我们知道编译以后如何加载。code模块定义了这些函数:


    purge(Module) -> true | false

    Types:

    Module = atom()

    Purges the code for Module, that is, removes code marked as old. If some processes still linger in the old code, these processes are killed before the code is removed.

    Returns true if successful and any process needed to be killed, otherwise false.

purge函数把现有的代码移去并标记为老版本,如果有任何process在使用旧代码,那么这些process将被杀死。注意尽管purge总是成功的,但是它的返回值只有在任何process需要被杀死的情况下才会返回true.

    load_binary(Module, Filename, Binary) -> {module, Module} | {error, What}

    Types:

    Module = atom()
    Filename = string()
    What = sticky_directory | badarg | term()

    This function can be used to load object code on remote Erlang nodes. It can also be used to load object code where the file name and module name differ. This, however, is a very unusual situation and not recommended. The parameter Binary must contain object code for Module. Filename is only used by the code server to keep a record of from which file the object code for Module comes. Accordingly, Filename is not opened and read by the code server.

    Returns {module, Module} if successful, or {error, sticky_directory} if the object code resides in a sticky directory, or {error, badarg} if any argument is invalid. Also if the loading fails, an error tuple is returned. See erlang:load_module/2 for possible values of What.

load_binary加载编译好的对象码,从而使得Module可以被程序使用。如果对象代码存在于sticky目录下的话,可能无法成功替换。sticky目录是erlang自己的运行时系统,包括kernel、stdlib和compiler,为了保证erlang的运行正常,缺省情况下这些目录是受保护的,被认为是sticky的。

组合起来

利用我们前面讨论过的内容,我们可以进行完整的试验.
假设现在有以下程序simplest.erl:


-module(simplest).
-export([foo/0]).
foo() ->
 io:format("foo~n").

我们用erl一步一步进行试验。


1> c(simplest,[debug_info]).
{ok,simplest}
2> simplest:foo().
foo
ok
3> simplest:test().

=ERROR REPORT==== 18-Aug-2006::15:06:17 ===
Error in process <0.32.0> with exit value: {undef,[{simplest,test,[]},{erl_eval,do_apply,5},{shell,exprs,6},{shell,eval_loop,3}]}

** exited: {undef,[{simplest,test,[]},
               {erl_eval,do_apply,5},
               {shell,exprs,6},
               {shell,eval_loop,3}]} **
4> {ok, Tokens, EndLine} = erl_scan:string("test() -> ok.").
{ok,[{atom,1,test},{'(',1},{')',1},{'->',1},{atom,1,ok},{dot,1}],1}
5> {ok, Forms} = erl_parse:parse_form(Tokens).
{ok,{function,1,test,0,[{clause,1,[],[],[{atom,1,ok}]}]}}
6> NewForms = [{attribute, 1, module, simplest},{attribute, 2, export, [{test,0}]}, Forms].
[{attribute,1,module,simplest},
{attribute,2,export,[{test,0}]},
{function,1,test,0,[{clause,1,[],[],[{atom,1,ok}]}]}]
7> {ok,simplest,Binary} = compile:forms(NewForms).
{ok,simplest,
<<70,79,82,49,0,0,1,188,66,69,65,77,65,116,111,109,0,0,0,56,0,0,0,6,8,...>>}
8> code:purge(simplest).
false
9> code:load_binary(simplest,"simplest.erl",Binary).
{module,simplest}
10>  simplest:foo().

=ERROR REPORT==== 18-Aug-2006::15:08:13 ===
Error in process <0.40.0> with exit value: {undef,[{simplest,foo,[]},{erl_eval,do_apply,5},{shell,exprs,6},{shell,eval_loop,3}]}

** exited: {undef,[{simplest,foo,[]},
               {erl_eval,do_apply,5},
               {shell,exprs,6},
               {shell,eval_loop,3}]} **
11> simplest:test().
ok
12>
结果是新加入了一个export的test/0函数,原先的foo/0函数没有了。

 

本文来自优快云博客,转载请标明出处:http://blog.youkuaiyun.com/minyskirt/archive/2009/12/13/4996838.aspx

dynamic-form-grid-editable.html <ti-tabs class="dynamic-form-grid-editable-container"> <ti-tab [id]="tabs[0].id" [header]="tabs[0].title" [(active)]="tabs[0].active" (activeChange)="onActiveChange($event, 0)"> <!-- 整体使用方式 --> <ti-dynamic-form *ngIf="tabs[0].formGroup" [model]="tabs[0].formModel" [group]="tabs[0].formGroup" (customEvent)="onCustomEvent($event)" > <ng-template modelId="grid-with-batch-edit-global" as="dynamicGridHeaderTemplate"> <ti-header-operators [items]="operationItems" [selectedRows]="selectedRows"></ti-header-operators> </ng-template> </ti-dynamic-form> <span style="display: inline-block; margin: 24px 0 0 24px" [tiTip]="model.btnDisabled ? model.editingTip : ''"> <button tiButton (click)="getCurrentFormValue()" [disabled]="model.btnDisabled">获取表单值</button> </span> <demo-log [logs]="logs"></demo-log> </ti-tab> <ti-tab [id]="tabs[1].id" [header]="tabs[1].title" [(active)]="tabs[1].active" (activeChange)="onActiveChange($event,1)"> <ti-container *ngIf="tabs[1].active"> <ti-formfield labelWidth="160px"> <ti-item> <ti-header-operators class="mb-3" [items]="operationItems1" [selectedRows]="selectedRows1"></ti-header-operators> <!-- 单独使用方式 --> <ti-dynamic-grid *ngIf="tabs[1].formGroup" [model]="tabs[1].formModel" [group]="tabs[1].formGroup" (gridRowSelected)="onGridRowSelected($event)" > </ti-dynamic-grid> </ti-item> </ti-formfield> </ti-container> <span style="display: inline-block; margin: 24px 0 0 24px" [tiTip]="model.btnDisabled ? model.editingTip : ''"> <button tiButton (click)="getCurrentFormValue()" [disabled]="model.btnDisabled">获取表单值</button> </span> <demo-log [logs]="logs"></demo-log> </ti-tab> </ti-tabs> DynamicFormGridEditableComponent.ts import { Component, Injector } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { TiHeaderOperatorsItem, GridPipeRenderTemplate, ICellRendererParams } from '@cloud/tinycloud'; import { TiDynamicGridModel, TiDynamicFormCheckService, TiDynamicFormModel, TiDynamicInputModel, TiDynamicSelectModel, TiDynamicFormControlModel, TiGridKitEditEvents, TI_GRID_KIT_ROW_ACTION_TYPE, TiDynamicFormGroupModel, MATCH_HIDDEN, MATCH_VALIDATOR_ADD, MATCH_DISABLED } from '@cloud/ti-dynamic-form'; import { TiValidators } from '@cloud/tiny3'; import { DynamicFormGridEditableModel } from './DynamicFormGridEditableModel'; import { DynamicFormBaseForComponent } from '../base-component/DynamicFormBaseForComponent'; @Component({ templateUrl: './dynamic-form-grid-editable.html', // 整体使用场景直接调用TiDynamicFormComponent示例的checkForm/checkFormAsync方法触发校验并聚焦到第一个错误表单项的行为 // 单独使用场景需要在使用组件的provider上注入TiDynamicFormCheckService // 再调用service上的checkForm/checkFormAsync方法触发校验并聚焦到第一个错误表单项的行为 providers: [TiDynamicFormCheckService, DynamicFormGridEditableModel], styles: [ ` .dynamic-form-grid-editable-container { background-color: var(--ti-color-bg-container-secondary); padding-bottom: var(--ti-common-space-2x); } ` ] }) export class DynamicFormGridEditableComponent extends DynamicFormBaseForComponent { /** * 批量选中的含数据 */ selectedRows: Array<any> = []; /** * 单独使用时批量选中的行数据 */ selectedRows1: Array<any> = []; /** * 操作按钮区域 */ operationItems: Array<TiHeaderOperatorsItem>; /** * 单独使用时批量选中的操作区 */ operationItems1: Array<TiHeaderOperatorsItem>; constructor(protected injector: Injector, public model: DynamicFormGridEditableModel) { super(injector); this.operationItems = this.getBatchOperationItems(); this.operationItems1 = [...this.operationItems]; } ngOnInit(): void { super.ngOnInit(); } getModelsForDynamicForm(): TiDynamicFormModel { const config: any = this.getCommonConfig(); return [new TiDynamicGridModel({ ...config })]; } getModelsForSingleUse(): TiDynamicFormControlModel { const config: any = this.getCommonConfig(true); return new TiDynamicGridModel({ ...config }) as TiDynamicFormControlModel; } // 整体使用单独使用时公用的配置项,业务时间开发过程中只使用其中一种模式 // isSingle:是否是单独使用模式,用于区分一些配置项,防止两种模式下相互影响 getCommonConfig(isSingle: boolean = false): any { const id: string = isSingle ? 'grid-with-batch-edit-single' : 'grid-with-batch-edit-global'; return { id, label: '批量编辑表格', // 动态表单开启编辑能力 supportEdit: true, gridKitOption: { enableColsResponsive: true, rowSelection: 'multiple', colDefs: this.model.getColDefs(false), srcData: this.model.getGridData(1) // displayMode: 'ti-grid' }, // 编辑态使用 groupFactory: (): TiDynamicFormModel => { return [ new TiDynamicInputModel({ id: 'protocol', value: '容许', validators: { required: null } }), new TiDynamicSelectModel({ id: 'type', value: 'IPV4', validators: { required: null }, options: [ { label: 'IPV4', value: 'IPV4' }, { label: 'IPV6', value: 'IPV6' } ], relations: [ { match: MATCH_DISABLED, when: [{ id: 'protocol', value: '' }] } ] }), // 协议端口 new TiDynamicFormGroupModel({ id: 'protocolPort', group: [ new TiDynamicSelectModel<string>({ id: 'protocol', options: [ { value: 'HTTP', label: 'HTTP' }, { value: 'TCP', label: 'TCP' } ], defaultValueIndex: 0 }), new TiDynamicInputModel({ id: 'sessionTime', placeholder: '请输入保持会话时间', descriptions: 'HTTP保持会话时间区域是1-1440,TCP保持会话的时间是1-60', // 根据表单值动态添加的校验器 dynamicValidators: { // 如果relation匹配成功,使用该校验规则 match: { rangeValue: { validator: TiValidators.rangeValue(1, 1440) } }, // 如果relation匹配不成功,使用该校验规则 notMatch: { rangeValue: { validator: TiValidators.rangeValue(1, 60) } } }, // 校验规则,始终需要的校验规则 validators: { // 设置为null,会调用TiValidators.required进行校验; required: null }, // 报错信息配置,可选项 errorMessages: { rangeValue: '当前协议的会话时间范围是:{0} - {1}' }, relations: [ { // 如果匹配成功,动态添加相关校验规则 match: MATCH_VALIDATOR_ADD, // 当是layer是L7的后端协议,启用相关校验规则 when: [{ id: 'protocol', value: 'HTTP' }] }, { match: MATCH_HIDDEN, when: [ { value: 'IPV4', /** * 13.0.180 版本新增,用在formarraydynamic-grid组件中本行的formGroup其他control建立关联关系 * 解决之前通过rootPath(id.index.id)中行新增删除时index未同步变化时(groupFactory只能在初始化时调用一次)引起的关联关系不正确的问题 * * 注意:如果存在formarray里面嵌套formarry类,可能会存在查找到的行group不准确的场景 * 查找逻辑:当前control往上递归,如果其父元素是个formArray,则认为该group为要找的行group */ currentRowPath: 'type' } ] } ] }) ] }) ]; }, // 行数据限制 limit: this.model.gridDataLimit$, initialCount: 5, // 配置触发表格刷新添加删除事件的流: 组件内部会监听这个流去触发对应的行为。 actionEvents: { // 行操作流 rowAction: this.model.rowAction$, // 编辑态流 editAction: this.model.editAction$, // 刷新单元格流 refreshCells: this.model.refreshCells$ } }; } getBatchOperationItems(): Array<TiHeaderOperatorsItem> { return [ { label: '迁移', disabled: (selectedRows: Array<any>): boolean => !!this.model.editRows$.getValue().length || !selectedRows?.length, getTip: (selectedRows: Array<any>): string => !selectedRows?.length ? '请至少选择一行' : this.model.editRows$.getValue().length ? this.model.editingTip : '', click: (): void => { this.logs = [...this.logs, `迁移按钮被点击`]; } }, { label: '修改策略', disabled: (selectedRows: Array<any>): boolean => !selectedRows?.length, hide: this.model.editRows$.pipe(map((value: any[]) => !!value.length)), getTip: (selectedRows: Array<any>): string => (!selectedRows?.length ? '请至少选择一行' : ''), click: ($event: any): void => { this.model.editAction$.next({ // 当前修改的行数据 rowDatas: $event.selectedRows, // 进入修改态 actionType: TiGridKitEditEvents.EDITING, callback: ({ editRows }: any): void => { this.model.editRows$.next(editRows); this.model.getBtnDisabled(editRows); } }); } }, { label: '保存修改', hide: this.model.editRows$.pipe(map((value: any[]) => !value.length)), disabled: (selectedRows: Array<any>): any => this.model.getBatchDisabled(selectedRows), click: ($event: any): void => { this.model.editAction$.next({ // 当前修改的行数据 rowDatas: $event.selectedRows, // 确定修改 actionType: TiGridKitEditEvents.CONFIRM_EDIT, // 如果在保存时需要在校验通过后先向后台请求保存,可以使用该接口。根据其返回的布尔值决定后续是否进行前台保存且 // 变成非编辑态(true: 进行后续;false或报错:不进行后续,停留在编辑态)。 // 13.0.120 版本新增 serverSideSaveFn: ({ editedRowDatas }: any): Observable<boolean> => { this.logs = [...this.logs, `编辑后的行数据集合:${JSON.stringify(editedRowDatas)}`]; return this.model.saveByMockApi(editedRowDatas); }, callback: ({ editRows }: any): void => { this.model.editRows$.next(editRows); this.model.getBtnDisabled(editRows); } }); } }, { label: '取消修改', hide: this.model.editRows$.pipe(map((value: any[]) => !value.length)), disabled: (selectedRows: Array<any>): any => this.model.getBatchDisabled(selectedRows), click: ($event: any): void => { this.model.editAction$.next({ // 当前修改的行数据 rowDatas: $event.selectedRows, // 取消修改 actionType: TiGridKitEditEvents.CANCEL_EDIT, callback: ({ editRows }: any): void => { this.model.editRows$.next(editRows); this.model.getBtnDisabled(editRows); } }); } }, { label: '添加', click: (): void => { const i: number = Math.floor(Math.random() * 10); this.model.rowAction$.next({ action: TI_GRID_KIT_ROW_ACTION_TYPE.ADD, // 新增行位置 addIndex: 0, // 新增行为编辑态 isEdit: true, data: { name: `名称-${i}`, protocol: `容许-${i}`, type: `${i % 2 ? 'IPV4' : 'IPV6'}`, protocolPort: { sessionTime: '80', protocol: 'HTTP' } } }); } } ]; } /** * 用户自定义事件监听 获取当前待编辑rows * @param customEventObj 自定义事件发射的数据 */ onCustomEvent(customEventObj: any): void { if (customEventObj?.type === 'gridRowSelected') { this.selectedRows = customEventObj.$event; } } /** * 单独使用时 获取当前待编辑rows * @param 自定义事件发射的行数据 */ onGridRowSelected($event: Array<any>): void { this.selectedRows1 = $event; } /** * 用于DEMO演示,业务无需关注 */ onActiveChange($event: any, index: number): void { super.onActiveChange($event, index); this.model.btnDisabled = false; this.logs = []; this.model.editRows$.next([]); } } /** * 自定义render组件 */ @Component({ selector: 'edit-component-render', template: ` <div #container tabindex="0" style="outline: none !important;color: red;" tiOverflow> <span>{{ getValue() }}</span> </div> `, host: { '[class.ti-render-container]': 'true' } }) export class customRenderer extends GridPipeRenderTemplate { public params: any; public input: string; agInit(params: ICellRendererParams): void { super.agInit(params); this.input = this.params.value ?? ''; } setValue(value: string): void { this.params.data[this.params.colDef.field] = value; } inputChange(): void { this.setValue(this.input); } keydown(event: any): void { event.stopPropagation(); } } DynamicFormGridEditableModel.ts import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable, Subject, of } from 'rxjs'; import { delay, map } from 'rxjs/operators'; import { gridConstants, TiGridKitColDef, TiGridOperationEvent, TiGridOperationOption, TiOperationRendererParams, TiRefreshScope, TiRenderType } from '@cloud/ti-grid-kit'; import { TI_GRID_KIT_ROW_ACTION_TYPE, TiDynamicGridLimit, TiGridKitEditEventCondition, TiGridKitEditEvents, TiGridKitRowActionCondition } from '@cloud/ti-dynamic-form'; @Injectable() export class DynamicFormGridEditableModel { /** * 表格行操作事件流。表格内部监听该事件流做相应的动作 */ rowAction$: Subject<TiGridKitRowActionCondition> = new Subject<TiGridKitRowActionCondition>(); /** * 单元格刷新流 */ refreshCells$: Subject<TiRefreshScope> = new Subject<TiRefreshScope>(); /** * 表格修改操作事件流。表格内部监听该事件处理表格修改。 */ editAction$: Subject<TiGridKitEditEventCondition> = new Subject<TiGridKitEditEventCondition>(); // 行数据限制 gridDataLimit$: BehaviorSubject<TiDynamicGridLimit> = new BehaviorSubject<TiDynamicGridLimit>({ min: 1 }); /** * 获取表单值按钮是否禁用 */ btnDisabled: boolean = false; /** * 编辑中禁用提示 */ editingTip: string = '请先保存或取消选中行的批量修改操作'; /** * 当前修改的行 */ editRows$: BehaviorSubject<any[]> = new BehaviorSubject<any[]>([]); /** * 删除按钮 */ deleteBtn: TiGridOperationOption = { label: '删除', key: 'op_delete', actionCallback: (obj: TiGridOperationEvent) => { this.rowAction$.next({ action: TI_GRID_KIT_ROW_ACTION_TYPE.REMOVE, // 推荐使用data传递删除数据,兼容之前的rowIndex逻辑 // rowIndex: obj.rowIndex data: obj.data }); } }; /** * 修改按钮 */ editBtn: TiGridOperationOption = { label: '修改策略', key: 'op-edit', actionCallback: (obj: TiGridOperationEvent): void => { this.editAction$.next({ // 当前修改的行数据 rowDatas: [obj.data], // 启动修改态 actionType: TiGridKitEditEvents.EDITING, callback: ({ editRows }: any): void => { this.editRows$.next(editRows); this.getBtnDisabled(editRows); } }); } }; /** * 确认修改 */ confirmEditBtn: TiGridOperationOption = { label: '保存', key: 'op-edit-cancel', actionCallback: (obj: TiGridOperationEvent): void => { this.editAction$.next({ // 当前修改的行数据 rowDatas: [obj.data], // 确定修改 actionType: TiGridKitEditEvents.CONFIRM_EDIT, // 如果在保存时需要在校验通过后先向后台请求保存,可以使用该接口。根据其返回的布尔值决定后续是否进行前台保存且 // 变成非编辑态(true: 进行后续;false或报错:不进行后续,停留在编辑态)。 // 13.0.120 版本新增 serverSideSaveFn: ({ editedRowDatas }: any): Observable<boolean> => { return this.saveByMockApi(editedRowDatas); }, callback: ({ editRows }: any): void => { this.editRows$.next(editRows); this.getBtnDisabled(editRows); } }); } }; /** * 取消修改 */ cancelEditBtn: TiGridOperationOption = { label: '取消', key: 'ti-grid-kit-op-edit-cancel', actionCallback: (obj: TiGridOperationEvent): void => { this.editAction$.next({ // 当前修改的行数据 rowDatas: [obj.data], // 取消修改 actionType: TiGridKitEditEvents.CANCEL_EDIT, callback: ({ editRows }: any): void => { this.editRows$.next(editRows); this.getBtnDisabled(editRows); } }); } }; // 获取列配置 getColDefs(singleEdit: boolean = true): TiGridKitColDef[] { return [ { headerName: '名称', field: 'name', // 可通过该字段关闭该列修改能力。 supportEdit: false }, { headerName: '策略', field: 'protocol', cellRenderer: TiRenderType.TextWithTipRenderer }, { headerName: '类型', field: 'type' }, { headerName: '后端协议', field: 'protocolPort', valueGetter: (params: any): string => { return params.data.protocolPort.protocol + ': ' + (params.data.protocolPort.sessionTime || '80'); } }, { headerName: '操作', field: gridConstants.OPERATION_FIELD, cellRenderer: TiRenderType.OperationRenderer, cellRendererParams: (params: any): TiOperationRendererParams => { const gridDataLimit: TiDynamicGridLimit = this.gridDataLimit$.getValue()!; const deleteBtn: TiGridOperationOption = this.deleteBtn; // 如果是单行修改,其他行的修改按钮就需要禁用 const hasEditRows: boolean = !!this.editRows$.getValue().length; const editDisabledTip: string = hasEditRows ? '请先保存或者取消处于修改状态的行' : ''; if (singleEdit) { this.editBtn.disabled = hasEditRows; this.editBtn.tipStr = editDisabledTip; } deleteBtn.disabled = hasEditRows; deleteBtn.tipStr = editDisabledTip; // 如果当前只有一行,删除按钮设置成禁用; if (params.context.parentArray.length <= (gridDataLimit?.min || 0)) { deleteBtn.disabled = true; deleteBtn.tipStr = '只有一条记录,不能删除。'; } return { ...params, operationList: params.data.isCurrentEdit ? [this.confirmEditBtn, this.cancelEditBtn] : [this.editBtn, deleteBtn] }; }, flex: 1 } ]; } getGridData(length: number = 2): Array<any> { const srcData: Array<any> = []; for (let i: number = 0; i < length; i++) { srcData.push({ name: `名称-${i}`, protocol: `容许-${i}`, type: `${i % 2 ? 'IPV4' : 'IPV6'}`, protocolPort: { sessionTime: '80', protocol: 'HTTP' } }); } return srcData; } // 获取表单值按钮禁用逻辑 getBtnDisabled(editRows: Array<any>): void { this.btnDisabled = !!editRows.length; } // 批量确认修改按钮批量取消修改按钮置灰逻辑 getBatchDisabled(selectRows: Array<any> = []): boolean { return !selectRows.length || !selectRows.filter((row: any) => row.isCurrentEdit).length; } // 模拟后台异步保存数据 // editedRowDatas 编辑后的行数据集合 saveByMockApi(editedRowDatas: Array<any>): Observable<boolean> { console.log('编辑后的行数据集合:', editedRowDatas); // return throwError('This is an error!'); // 测试报错的场景 return of(editedRowDatas).pipe( delay(800), map(() => { return true; // 后台保存成功 // return false; // 后台保存失败,跟上一行可切换测试 }) ); } } DynamicFormBaseComponent.ts import { Component, Injector, ViewChild } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { take } from 'rxjs/operators'; import { TiDynamicFormCheckService, TiDynamicFormComponent } from '@cloud/ti-dynamic-form'; /** * 动态表单demo示例基类,统一添加值校验能力 */ @Component({ template: '' }) export class DynamicFormBaseComponent { constructor(protected injector?: Injector) {} /** * 动态表单引用。可用于表单校验等场景 */ @ViewChild(TiDynamicFormComponent) public dynamicFormRef: TiDynamicFormComponent; /** * 日志打印记录,DEMO演示,业务无需关注 */ logs: Array<any> = []; /** * 根据校验流状态控制按钮禁用或者loading态,业务根据实际情况选用 */ btnDisabled$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); /** * 组件销毁流 */ destory$: Subject<void> = new Subject<void>(); checkAndGetFormValue(formGroup: FormGroup, narrowSelector?: string): void { let checkResult: boolean; if (this.dynamicFormRef && !narrowSelector) { // 整体使用场景直接调用TiDynamicFormComponent示例的checkForm/checkFormAsync方法触发校验并聚焦到第一个错误表单项的行为 checkResult = this.dynamicFormRef.checkForm(); } else { // 单独使用场景需要在使用组件的provider上注入TiDynamicFormCheckService // 再调用service上的checkForm/checkFormAsync方法触发校验并聚焦到第一个错误表单项的行为 checkResult = !!this.injector?.get(TiDynamicFormCheckService).checkForm(formGroup, narrowSelector); } if (!checkResult) { return; } this.logs = [...this.logs, `表单值:${JSON.stringify(formGroup.value)}`]; } /** * 异步校验能力 */ checkAndGetFormAsyncValue(formGroup: FormGroup, isSingle: boolean = false): void { // 按钮处于禁用态,不重复下发校验 if (this.btnDisabled$.getValue()) { return; } this.btnDisabled$.next(true); // 本案例核心配置项:checkFormAsync使用 let isValid$: Observable<boolean>; if (this.dynamicFormRef && !isSingle) { // 整体使用场景直接调用TiDynamicFormComponent示例的checkForm/checkFormAsync方法触发校验并聚焦到第一个错误表单项的行为 isValid$ = this.dynamicFormRef.checkFormAsync(); } else { // 单独使用场景需要在使用组件的provider上注入TiDynamicFormCheckService // 再调用service上的checkForm/checkFormAsync方法触发校验并聚焦到第一个错误表单项的行为 isValid$ = this.injector?.get(TiDynamicFormCheckService)?.checkFormAsync(formGroup)!; } isValid$ .pipe( // 注意:点击获取校验属于高频操作,及时取消订阅或采用take(1) take(1) ) .subscribe((isValid: boolean) => { this.btnDisabled$.next(false); if (isValid) { this.logs = [...this.logs, `表单值:${JSON.stringify(formGroup.value)}`]; } }); } ngOnDestroy(): void { this.destory$.next(); } } DynamicFormTypeBaseComponent.ts import { Component, Injector, OnInit } from '@angular/core'; import { MATCH_DISABLED, MATCH_HIDDEN, TiDynamicFormControlModel, TiDynamicFormControlSpaceType, TiDynamicFormGroupOption, TiDynamicFormModel, TiDynamicFormService, TiDynamicSwitchModel } from '@cloud/ti-dynamic-form'; import { DynamicFormBaseComponent } from './DynamicFormBaseComponent'; import { FormGroup } from '@angular/forms'; export enum DynamicFormCommonId { HIDDEN = 'common_hidden', DISABLE = 'common_disable', RESET = 'common_reset' } export interface DynamicFormDemoTab { title: string; active?: boolean; formGroup: FormGroup; formModel: TiDynamicFormControlModel | TiDynamicFormModel; id: string; } /** * 动态表单demo示例基类,统一添加整体使用/单独使用两种demo呈现逻辑 */ @Component({ template: '' }) export abstract class DynamicFormTypeBaseComponent extends DynamicFormBaseComponent implements OnInit { formModelForDynamicForm: TiDynamicFormModel; formModelForSingleUse: TiDynamicFormControlModel; formGroupForDynamicForm: FormGroup; formGroupForSingleUse: FormGroup; dynamicFormServ: TiDynamicFormService; groupOption: TiDynamicFormGroupOption; public tabs: Array<DynamicFormDemoTab> = []; // 是否需要开启测试表单的 “是否显示” “是否禁用” 功能 public needCommonModel: boolean = true; // 是否是表单组 public isFormGroup: boolean = false; constructor(protected injector: Injector) { super(injector); this.dynamicFormServ = injector.get(TiDynamicFormService); } ngOnInit(): void { this.setFormModel(); } setFormModel(): void { const isLocal: boolean = !window.location.href.includes('cloud-design/design-develop/'); this.formModelForDynamicForm = isLocal && this.needCommonModel ? [...this.getCommonModel(), ...this.getModelsForDynamicForm()] : [this.getFormModel()]; this.formModelForSingleUse = this.getModelsForSingleUse(); this.formGroupForDynamicForm = this.dynamicFormServ!.createFormGroup(this.formModelForDynamicForm); this.formGroupForSingleUse = this.dynamicFormServ!.createFormGroup([this.formModelForSingleUse]); this.tabs = [ { title: 'ti-dynamic-form整体使用方式', active: true, formModel: this.formModelForDynamicForm, formGroup: this.formGroupForDynamicForm, id: 'use_with_dynamic_form' }, { title: '在模板中单独使用selector方式', formModel: this.formModelForSingleUse, formGroup: this.formGroupForSingleUse, id: 'use_with_selector' } ]; } getCommonModel(): TiDynamicFormModel { return [ new TiDynamicSwitchModel({ id: DynamicFormCommonId.HIDDEN, label: '是否显示', value: true }), new TiDynamicSwitchModel({ id: DynamicFormCommonId.DISABLE, label: '是否禁用', value: false, spaceType: this.isFormGroup ? TiDynamicFormControlSpaceType.group : TiDynamicFormControlSpaceType.normal }) ]; } getModelsForDynamicForm(): TiDynamicFormModel { let formModel: TiDynamicFormControlModel = this.getFormModel(); formModel.relations = [ ...formModel.relations, { match: MATCH_HIDDEN, when: [ { id: DynamicFormCommonId.HIDDEN, value: false } ] }, { match: MATCH_DISABLED, when: [ { id: DynamicFormCommonId.DISABLE, value: true } ] } ]; return [formModel]; } getModelsForSingleUse(): TiDynamicFormControlModel { const formModel: TiDynamicFormControlModel = this.getFormModel('single'); formModel.id += 'single'; return formModel; } getCurrentFormValue(): any { const currentTab: any = this.tabs.find((tab) => tab.active === true); return this.checkAndGetFormValue(currentTab?.formGroup, `#${currentTab?.id}`); } abstract getFormModel(id?: string): TiDynamicFormControlModel; } dynamic-form-grid-editable.module.ts import { NgModule } from "@angular/core"; // tiny modules import { TiContainerModule, TiHeaderOperatorsModule } from "@cloud/tinycloud"; import { TiButtonModule, TiFormfieldModule, TiTabModule } from "@cloud/tiny3"; import { TiDynamicFormCoreModule, TiDynamicGridModule } from "@cloud/ti-dynamic-form"; import { DynamicFormGridEditableComponent } from "./DynamicFormGridEditableComponent"; @NgModule({ imports: [ TiContainerModule, TiHeaderOperatorsModule, TiButtonModule, TiFormfieldModule, TiTabModule, TiDynamicFormCoreModule, TiDynamicGridModule ], declarations: [DynamicFormGridEditableComponent], }) export class DynamicFormGridEditableModule {} 帮我分析这六个文件的关系
最新发布
07-12
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值