diff --git a/compiler/src/pre_define.ts b/compiler/src/pre_define.ts index a0a1f741c750189f39d8c620e805c0b843dfdb2c..33f4f2115bae818accc303df0ec8752cfa5ebdcd 100644 --- a/compiler/src/pre_define.ts +++ b/compiler/src/pre_define.ts @@ -35,13 +35,14 @@ export const COMPONENT_PROVIDE_DECORATOR: string = '@Provide'; export const COMPONENT_CONSUME_DECORATOR: string = '@Consume'; export const COMPONENT_OBJECT_LINK_DECORATOR: string = '@ObjectLink'; export const COMPONENT_WATCH_DECORATOR: string = '@Watch'; +export const COMPONENT_BUILDERPARAM_DECORATOR: string = '@BuilderParam'; export const INNER_COMPONENT_DECORATORS: Set = new Set([COMPONENT_DECORATOR_ENTRY, COMPONENT_DECORATOR_PREVIEW, COMPONENT_DECORATOR_COMPONENT, COMPONENT_DECORATOR_CUSTOM_DIALOG]); export const INNER_COMPONENT_MEMBER_DECORATORS: Set = new Set([COMPONENT_STATE_DECORATOR, COMPONENT_PROP_DECORATOR, COMPONENT_LINK_DECORATOR, COMPONENT_STORAGE_PROP_DECORATOR, COMPONENT_STORAGE_LINK_DECORATOR, COMPONENT_PROVIDE_DECORATOR, COMPONENT_CONSUME_DECORATOR, - COMPONENT_OBJECT_LINK_DECORATOR, COMPONENT_WATCH_DECORATOR]); + COMPONENT_OBJECT_LINK_DECORATOR, COMPONENT_WATCH_DECORATOR, COMPONENT_BUILDERPARAM_DECORATOR]); export const COMPONENT_OBSERVED_DECORATOR: string = '@Observed'; export const COMPONENT_BUILDER_DECORATOR: string = '@Builder'; @@ -181,6 +182,7 @@ export const GEOMETRY_VIEW: string = 'GeometryView'; export const MODULE_SHARE_PATH: string = 'src' + path.sep + 'ets' + path.sep + 'share'; export const BUILD_SHARE_PATH: string = '../share'; +export const CHILD: string = 'child'; export const THIS: string = 'this'; export const STYLES: string = 'Styles'; export const VISUAL_STATE: string = 'visualState'; diff --git a/compiler/src/process_component_build.ts b/compiler/src/process_component_build.ts index da3beb5023948b134dc685a5d589097055141036..29c04bc77295edc6faa39a13afa16818cc52b363 100644 --- a/compiler/src/process_component_build.ts +++ b/compiler/src/process_component_build.ts @@ -45,6 +45,7 @@ import { COMPONENT_TRANSITION_NAME, COMPONENT_DEBUGLINE_FUNCTION, ATTRIBUTE_STATESTYLES, + CHILD, THIS, VISUAL_STATE, VIEW_STACK_PROCESSOR, @@ -76,6 +77,7 @@ import { componentInfo, createFunction } from './utils'; +import { builderParamObjectCollection } from './process_component_member'; import { projectConfig } from '../main'; import { transformLog, contextGlobal } from './process_ui_syntax'; import { props } from './compile_info'; @@ -157,20 +159,25 @@ function validateRootNode(node: ts.MethodDeclaration, log: LogInfo[]): boolean { export function processComponentChild(node: ts.Block | ts.SourceFile, newStatements: ts.Statement[], log: LogInfo[]): void { if (node.statements.length) { - node.statements.forEach((item, index) => { + node.statements.forEach((item, index, array) => { if (ts.isExpressionStatement(item)) { const name: string = getName(item); switch (getComponentType(item, log, name)) { case ComponentType.innerComponent: - processInnerComponent(item, index, Array.from(node.statements), newStatements, log, name); + processInnerComponent(item, index, Array.from(node.statements), + newStatements, log, name); break; case ComponentType.customComponent: + if (index + 1 < array.length && ts.isBlock(array[index + 1])) { + item = processBlockChange(item, + array[index + 1] as ts.Block, log) + } processCustomComponent(item, newStatements, log); break; case ComponentType.forEachComponent: processForEachComponent(item, newStatements, log); break; - case ComponentType.customBuilderMethod: + case ComponentType.customBuilderMethod || ComponentType.builderParamMethod: newStatements.push(item); break; } @@ -188,6 +195,31 @@ export function processComponentChild(node: ts.Block | ts.SourceFile, newStateme } } +function processBlockChange(node: ts.ExpressionStatement, nextNode: ts.Block, + log: LogInfo[]): ts.block { + // @ts-ignore + const newBlock: ts.Block = processComponentBlock(nextNode, false, log); + const arrowNode: ts.ArrowFunction = ts.factory.createArrowFunction(undefined, undefined, + [], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), newBlock); + const newPropertyAssignment:ts.PropertyAssignment = ts.factory.createPropertyAssignment( + ts.factory.createIdentifier(CHILD), arrowNode); + // @ts-ignore + let argumentsArray: ts.ObjectLiteralExpression[] = node.express.arguments; + if (arguments && arguments.length < 1) { + argumentsArray = [ts.factory.createObjectLiteralExpression([newPropertyAssignment], true)] + } else { + // @ts-ignore + argumentsArray = [ts.factory.createObjectLiteralExpression( + // @ts-ignore + node.express.arguments[0].properties.concat([newPropertyAssignment]), true)] + } + // @ts-ignore + node = ts.factory.updateExpressionStatement(node, ts.factory.updateCallExpression(node.expression, + // @ts-ignore + node.expression.expression, node.expression.expression,typeArguments, argumentsArray)) + return node; +} + function processInnerComponent(node: ts.ExpressionStatement, index: number, arr: ts.Statement[], newStatements: ts.Statement[], log: LogInfo[], name: string): void { const res: CreateResult = createComponent(node, COMPONENT_CREATE_FUNCTION); @@ -817,7 +849,8 @@ enum ComponentType { innerComponent, customComponent, forEachComponent, - customBuilderMethod + customBuilderMethod, + builderParamMethod } function getComponentType(node: ts.ExpressionStatement, log: LogInfo[], @@ -831,7 +864,10 @@ function getComponentType(node: ts.ExpressionStatement, log: LogInfo[], return ComponentType.forEachComponent; } else if (CUSTOM_BUILDER_METHOD.has(name)) { return ComponentType.customBuilderMethod; - } else if (!isAttributeNode(node)) { + } else if (builderParamObjectCollection.get(componentCollection.currentClassName) && + builderParamObjectCollection.get(componentCollection.currentClassName).has(name)) { + return ComponentType.builderParamMethod; + }else if (!isAttributeNode(node)) { log.push({ type: LogType.ERROR, message: `'${node.getText()}' does not meet UI component syntax.`, diff --git a/compiler/src/process_component_class.ts b/compiler/src/process_component_class.ts index 14033256e2fa13e12f2be7f2a1c340fb72f75ebd..4df767460da56f6efec11c4c2b52ab2c88594bd8 100644 --- a/compiler/src/process_component_class.ts +++ b/compiler/src/process_component_class.ts @@ -240,7 +240,9 @@ function validateBuilderFunctionNode(node: ts.PropertyAccessExpression | ts.Iden ts.isIdentifier(node) && CUSTOM_BUILDER_METHOD.has(node.escapedText.toString())) && !((ts.isPropertyAccessExpression(node) && validateBuilderParam(node)) || (ts.isIdentifier(node) && node.parent && ts.isPropertyAccessExpression(node.parent) && - validateBuilderParam(node.parent)))) { + validateBuilderParam(node.parent))) && + // @ts-ignore + (!node.parent.arguments || (node.parent.arguments && !node.parent.arguments.length))) { return true; } else { return false; diff --git a/compiler/src/process_component_member.ts b/compiler/src/process_component_member.ts index 4799fe32de464adf07ea3bd0b30c6dcad2c654f9..1dc9b50daa1b229d1baba3c59b4953fb01e59213 100644 --- a/compiler/src/process_component_member.ts +++ b/compiler/src/process_component_member.ts @@ -51,7 +51,8 @@ import { JS_DIALOG, CUSTOM_DIALOG_CONTROLLER_BUILDER, BASE_COMPONENT_NAME, - COMPONENT_CREATE_FUNCTION + COMPONENT_CREATE_FUNCTION, + COMPONENT_BUILDERPARAM_DECORATOR } from './pre_define'; import { forbiddenUseStateType, @@ -98,7 +99,9 @@ export const mandatoryToInitViaParamDecorators: Set = new Set([...propAndLinkDecorators, COMPONENT_OBJECT_LINK_DECORATOR]); export const setUpdateParamsDecorators: Set = - new Set([...observedPropertyDecorators, COMPONENT_PROP_DECORATOR, COMPONENT_OBJECT_LINK_DECORATOR]); + new Set([...observedPropertyDecorators, COMPONENT_PROP_DECORATOR, COMPONENT_OBJECT_LINK_DECORATOR, + COMPONENT_BUILDERPARAM_DECORATOR + ]); export const immutableDecorators: Set = new Set([COMPONENT_STORAGE_PROP_DECORATOR, COMPONENT_OBJECT_LINK_DECORATOR]); @@ -110,6 +113,8 @@ export const decoratorParamSet: Set = new Set(); export const stateObjectCollection: Set = new Set(); +export const builderParamObjectCollection: Map> = new Map(); + export class UpdateResult { private itemUpdate: boolean = false; private ctorUpdate: boolean = false; @@ -263,7 +268,8 @@ function processPropertyNodeDecorator(parentName: ts.Identifier, node: ts.Proper if (node.questionToken && mandatoryToInitViaParamDecorators.has(decoratorName)) { validateHasIllegalQuestionToken(name, decoratorName, log); } - if (!isSimpleType(node.type, program)) { + if (!isSimpleType(node.type, program) && + decoratorName !== COMPONENT_BUILDERPARAM_DECORATOR) { stateObjectCollection.add(name.escapedText.toString()); } if (decoratorName === COMPONENT_WATCH_DECORATOR && @@ -405,6 +411,16 @@ function createUpdateParams(name: ts.Identifier, decorator: string): ts.Statemen case COMPONENT_OBJECT_LINK_DECORATOR: updateParamsNode = createUpdateParamsWithSet(name); break; + case COMPONENT_BUILDERPARAM_DECORATOR: + if (decorator === COMPONENT_BUILDERPARAM_DECORATOR) { + if (!builderParamObjectCollection.get(componentCollection.currentClassName)) { + builderParamObjectCollection.set(componentCollection.currentClassName, new Set([])); + } + builderParamObjectCollection.get(componentCollection.currentClassName) + .add(name.escapedText.toString()) + } + updateParamsNode = createUpdateParamsWithoutIf(name, 1); + break; } return updateParamsNode; } @@ -419,9 +435,15 @@ function createUpdateParamsWithIf(name: ts.Identifier): ts.IfStatement { createUpdateParamsWithoutIf(name)], true), undefined); } -function createUpdateParamsWithoutIf(name: ts.Identifier): ts.ExpressionStatement { +function createUpdateParamsWithoutIf(name: ts.Identifier, isAdd: number = 0): ts.ExpressionStatement { + let textName: string; + if (isAdd) { + textName = `__${name.getText()}`; + } else { + textName = name.getText(); + } return ts.factory.createExpressionStatement(ts.factory.createBinaryExpression( - createPropertyAccessExpressionWithThis(name.getText()), + createPropertyAccessExpressionWithThis(textName), ts.factory.createToken(ts.SyntaxKind.EqualsToken), createPropertyAccessExpressionWithParams(name.getText()))); } diff --git a/compiler/src/process_custom_component.ts b/compiler/src/process_custom_component.ts index 05b107db5843a8c9992dbb4036809a260c2a6d57..5ac3f84b1f6032b33e4c7bb70381d12a11194418 100644 --- a/compiler/src/process_custom_component.ts +++ b/compiler/src/process_custom_component.ts @@ -105,7 +105,10 @@ function validateCustomComponentPrams(node: ts.ExpressionStatement, name: string ts.isObjectLiteralExpression(nodeArguments[0])) { const nodeArgument: ts.ObjectLiteralExpression = nodeArguments[0] as ts.ObjectLiteralExpression; nodeArgument.properties.forEach(item => { - curChildProps.add(item.name.getText()); + if (item.name && item.name.escapedText) { + // @ts-ignore + curChildProps.add(item.name.escapedText.toString()); + } if (isThisProperty(item, propertySet)) { validateStateManagement(item, name, log); if (isNonThisProperty(item, linkSet)) { @@ -157,7 +160,11 @@ function validateStateManagement(node: ts.ObjectLiteralElementLike, customCompon function checkFromParentToChild(node: ts.ObjectLiteralElementLike, customComponentName: string, log: LogInfo[]): void { - const propertyName: string = node.name.getText(); + let propertyName: string; + if (node.name && node.name.escapedText) { + // @ts-ignore + propertyName = node.name.escapedText.toString(); + } const curPropertyKind: string = getPropertyDecoratorKind(propertyName, customComponentName); if (curPropertyKind) { if (isInitFromParent(node)) { diff --git a/compiler/test/ut/builder/builderLambda.ts b/compiler/test/ut/builder/builderLambda.ts new file mode 100644 index 0000000000000000000000000000000000000000..c19735078ef8751b37319aad28a0b793158db047 --- /dev/null +++ b/compiler/test/ut/builder/builderLambda.ts @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +exports.source = ` +@Component +struct CustomContainer { + header: string = ""; + footer: string = ""; + @BuilderParam child: () => any; + + build() { + Column() { + Text(this.header) + this.child() + Text(this.footer) + } + } +} + +@Builder function specificParam(label1: string, label2: string) { + Column() { + Text(label1) + Text(label2) + } +} + +@Entry +@Component +struct CustomContainerUser { + build() { + Column() { + CustomContainer({header: "Header", footer: "Footer"}){ + Column() { + Text("content1") + .width(50) + Text("content2") + } + specificParam("content3", "content4") + } + } + } +} +` +exports.expectResult = +`class CustomContainer extends View { + constructor(compilerAssignedUniqueChildId, parent, params) { + super(compilerAssignedUniqueChildId, parent); + this.header = "" + this.footer = "" + this.updateWithValueParams(params); + } + updateWithValueParams(params) { + if (params.header !== undefined) { + this.header = params.header; + } + if (params.footer !== undefined) { + this.footer = params.footer; + } + this.__child = params.child; + } + aboutToBeDeleted() { + this.__child.aboutToBeDeleted(); + SubscriberManager.Get().delete(this.id()); + } + get child(){ + return this.__child.get(); + } + set child(newValue) { + this.__child.set(newValue) + } + render() { + Column.create(); + Text.create(this.header); + Text.pop(); + this.child(); + Text.create(this.footer); + Text.pop(); + Column.pop(); + } +} +function specificParam(label1, label2) { + Column.create(); + Text.create(label1); + Text.pop(); + Text.create(label1); + Text.pop(); + Column.pop(); +} +class CustomContainerUser { + constructor(compilerAssignedUniqueChildId, parent, params) { + super(compilerAssignedUniqueChildId, parent); + this.updateWithValueParams(params); + } + updateWithValueParams(params) { + } + aboutToBeDeleted() { + SubscriberManager.Get().delete(this.id()); + } + render() { + Column.create(); + let earlierCreatedChild_2 = this.findChildById("2"); + if (earlierCreatedChild_2 == undefined) { + View.create(new CustomContainer("2", this, { + header: "Header", footer: "Footer", + child: () => { + Column.create(); + Text.create("content1"); + Text.width(50) + Text.pop(); + Text.create("content2"); + Text.pop(); + Column.pop(); + specificParam("content3", "content4) + } + })); + } + else { + earlierCreatedChild_2.updateWithValueParams({ + header: "Header", footer: "Footer", + child: () => { + Column.create(); + Text.create("content1"); + Text.width(50) + Text.pop(); + Text.create("content2"); + Text.pop(); + Column.pop(); + specificParam("content3", "content4) + }} + }); + View.create(earlierCreatedChild_2); + } + Column.pop(); + } +} +loadDocument(new MyComponent("1", undefined, {})); +` diff --git a/compiler/test/ut/builder/builderParam.ts b/compiler/test/ut/builder/builderParam.ts new file mode 100644 index 0000000000000000000000000000000000000000..e3c8083e1e91df0f660d2b64dabf07b6df4e39ef --- /dev/null +++ b/compiler/test/ut/builder/builderParam.ts @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +exports.source = ` +@Component +struct CustomContainer { + header: string = ""; + footer: string = ""; + @BuilderParam child: () => any; + + build() { + Column() { + Text(this.header) + this.child() + Text(this.footer) + } + } +} + +@Builder function specificParam(label1: string, label2: string) { + Column() { + Text(label1) + Text(label2) + } +} + +@Entry +@Component +struct CustomContainerUser { + @Builder specificChild() { + Column() { + Text("My content1") + Text("My content1") + } + } + + build() { + Column() { + CustomContainer({ + header: "Header", + footer: "Footer", + child: this.specificChild + }) + CustomContainer({ + header: "Header", + footer: "Footer", + child: specificParam("content3", "content4") + }) + } + } +} +` +exports.expectResult = +`class CustomContainer extends View { + constructor(compilerAssignedUniqueChildId, parent, params) { + super(compilerAssignedUniqueChildId, parent); + this.header = "" + this.footer = "" + this.updateWithValueParams(params); + } + updateWithValueParams(params) { + if (params.header !== undefined) { + this.header = params.header; + } + if (params.footer !== undefined) { + this.footer = params.footer; + } + this.__child = params.child; + } + aboutToBeDeleted() { + this.__child.aboutToBeDeleted(); + SubscriberManager.Get().delete(this.id()); + } + get child(){ + return this.__child.get(); + } + set child(newValue) { + this.__child.set(newValue) + } + render() { + Column.create(); + Text.create(this.header); + Text.pop(); + this.child(); + Text.create(this.footer); + Text.pop(); + Column.pop(); + } +} +function specificParam(label1, label2) { + Column.create(); + Text.create(label1); + Text.pop(); + Text.create(label1); + Text.pop(); + Column.pop(); +} +class CustomContainerUser { + constructor(compilerAssignedUniqueChildId, parent, params) { + super(compilerAssignedUniqueChildId, parent); + this.updateWithValueParams(params); + } + updateWithValueParams(params) { + } + aboutToBeDeleted() { + SubscriberManager.Get().delete(this.id()); + } + specificChild() { + Column.create(); + Text.create("My content1"); + Text.pop(); + Text.create("My content2"); + Text.pop(); + Column.pop(); + } + render() { + Column.create(); + let earlierCreatedChild_2 = this.findChildById("2"); + if (earlierCreatedChild_2 == undefined) { + View.create(new CustomContainer("2", this, { + header: "Header", + footer: "Footer", + child: {build:this.specificChild.bind(this)} + })); + } + else { + earlierCreatedChild_2.updateWithValueParams({ + header: "Header", + footer: "Footer", + child: {build:this.specificChild.bind(this)} + }); + View.create(earlierCreatedChild_2); + } + let earlierCreatedChild_3 = this.findChildById("3"); + if (earlierCreatedChild_3 == undefined) { + View.create(new CustomContainer("3", this, { + header: "Header", + footer: "Footer", + child: specificParam("content3", "content4") + })); + } + else { + earlierCreatedChild_3.updateWithValueParams({ + header: "Header", + footer: "Footer", + child: specificParam("content3", "content4") + }); + View.create(earlierCreatedChild_3); + } + Column.pop(); + } +} +loadDocument(new MyComponent("1", undefined, {})); +`