diff --git a/ets-tests/ets/AllPages.ets b/ets-tests/ets/AllPages.ets index ef1ef34fd0024083bd0f08db51e8b447accbccaf..7dc4ef13e4f38c003da797b333a66a2a82d56058 100644 --- a/ets-tests/ets/AllPages.ets +++ b/ets-tests/ets/AllPages.ets @@ -16,6 +16,7 @@ import { InitializationFromParent } from "./pages/states/InitializationFromParen import { StateIncrement } from "./pages/states/StateIncrement" import { StateDecrement } from "./pages/states/StateDecrement" import { UndefinedStateVariable } from "./pages/states/UndefinedStateVariable" +import { ObservableObject } from "./pages/states/ObservableObject" import { ObservableDate } from "./pages/states/ObservableDate" import { ObservableArray } from "./pages/states/ObservableArray" import { ObservableDateArray } from "./pages/states/ObservableDateArray" @@ -86,6 +87,7 @@ function pageByName(name: string): void { case "InitializationFromParent": InitializationFromParent(); break case "LinkDecorator": LinkDecorator(); break case "UndefinedStateVariable": UndefinedStateVariable(); break + case "ObservableObject": ObservableObject(); break case "ObservableDate": ObservableDate(); break case "ObservableArray": ObservableArray(); break case "ObservableDateArray": ObservableDateArray(); break diff --git a/ets-tests/ets/pages/states/ObservableObject.ets b/ets-tests/ets/pages/states/ObservableObject.ets new file mode 100644 index 0000000000000000000000000000000000000000..c806cf611df51f470d83bd4976b55801272c166f --- /dev/null +++ b/ets-tests/ets/pages/states/ObservableObject.ets @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025 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. + */ + +import { TestComponent } from '@ohos.arkui' + +class Person { + name: string + age: number + + constructor(name: string, age: number) { + this.name = name + this.age = age + } +} + +@Component +struct ObservableObject { + @State person: Person = new Person("Alice", 3) + + build() { + TestComponent({}) + .log(`name = ${this.person.name}, age = ${this.person.age}`) + + TestComponent({ id: 11 }).onChange(() => { this.person.age = 4 }) + TestComponent({ id: 12 }).onChange(() => { this.person.name = "Bob" }) + TestComponent({ id: 13 }).onChange(() => { this.person = new Person("Alice", 5) }) + TestComponent({ id: 14 }).onChange(() => { this.person.age = 6 }) + } +} diff --git a/ets-tests/ets/suites/LinkDecorator.ets b/ets-tests/ets/suites/LinkDecorator.ets index aa50ede992758a4b5b36394118ff3de57666361e..f8733a9ed15c4be394bbb41ada223c3353fadfee 100644 --- a/ets-tests/ets/suites/LinkDecorator.ets +++ b/ets-tests/ets/suites/LinkDecorator.ets @@ -200,8 +200,8 @@ export function linkDecoratorBehaviorTest(control: AppControl) { + "LinkSetChildComponent: \"text\"\n" + "LinkObjectChildComponent: 0\n" + "LinkSetChildComponent: 0\n" - + "LinkObjectChildComponent: {\"age\":18}\n" - + "LinkSetChildComponent: {\"age\":18}\n", + + "LinkObjectChildComponent: {\"__age\":18}\n" + + "LinkSetChildComponent: {\"__age\":18}\n", "Object binding did not synchronize correctly between parent and child") }) test("Boolean binding synchronization between parent and child", () => { diff --git a/ets-tests/ets/suites/ProvideConsume.ets b/ets-tests/ets/suites/ProvideConsume.ets index f52bb6cc622eb427ab9edc98b365713ba409888b..3dc390ff0f73e86429167c946d204c4398de31e3 100644 --- a/ets-tests/ets/suites/ProvideConsume.ets +++ b/ets-tests/ets/suites/ProvideConsume.ets @@ -175,23 +175,23 @@ export function provideConsumeTests(control: AppControl) { test("Support date, class, object, union types", () => { testPageOnLoad(control, "ProvideConsumeObject", "parent.date = 2025-4-1\n" - + "parent.objectValue = {\"age\":18}\n" - + "parent.classValue = {\"age\":18}\n" + + "parent.objectValue = {\"__age\":18}\n" + + "parent.classValue = {\"__age\":18}\n" + "parent.unionValue = \"simple\"\n" + "child.date = 2025-4-1\n" - + "child.objectValue = {\"age\":18}\n" - + "child.classValue = {\"age\":18}\n" + + "child.objectValue = {\"__age\":18}\n" + + "child.classValue = {\"__age\":18}\n" + "child.unionValue = \"simple\"\n") }) test("Two-way synchronization after changing Date variable", () => { testPageOnChange(control, "ProvideConsumeObject", [11, 21], "parent.date = 2025-4-1\n" - + "parent.objectValue = {\"age\":18}\n" - + "parent.classValue = {\"age\":18}\n" + + "parent.objectValue = {\"__age\":18}\n" + + "parent.classValue = {\"__age\":18}\n" + "parent.unionValue = \"simple\"\n" + "child.date = 2025-4-1\n" - + "child.objectValue = {\"age\":18}\n" - + "child.classValue = {\"age\":18}\n" + + "child.objectValue = {\"__age\":18}\n" + + "child.classValue = {\"__age\":18}\n" + "child.unionValue = \"simple\"\n" + "child.date = 2025-3-1\n" @@ -202,12 +202,12 @@ export function provideConsumeTests(control: AppControl) { test("Two-way synchronization after changing object variable", () => { testPageOnChange(control, "ProvideConsumeObject", [13, 23], "parent.date = 2025-4-1\n" - + "parent.objectValue = {\"age\":18}\n" - + "parent.classValue = {\"age\":18}\n" + + "parent.objectValue = {\"__age\":18}\n" + + "parent.classValue = {\"__age\":18}\n" + "parent.unionValue = \"simple\"\n" + "child.date = 2025-4-1\n" - + "child.objectValue = {\"age\":18}\n" - + "child.classValue = {\"age\":18}\n" + + "child.objectValue = {\"__age\":18}\n" + + "child.classValue = {\"__age\":18}\n" + "child.unionValue = \"simple\"\n" + "child.objectValue = 125\n" @@ -219,29 +219,29 @@ export function provideConsumeTests(control: AppControl) { testPageOnChange(control, "ProvideConsumeObject", [14, 24], [ 'parent.date = 2025-4-1', - 'parent.objectValue = {"age":18}', - 'parent.classValue = {"age":18}', + 'parent.objectValue = {"__age":18}', + 'parent.classValue = {"__age":18}', 'parent.unionValue = "simple"', 'child.date = 2025-4-1', - 'child.objectValue = {"age":18}', - 'child.classValue = {"age":18}', + 'child.objectValue = {"__age":18}', + 'child.classValue = {"__age":18}', 'child.unionValue = "simple"', - 'child.classValue = {"age":31}', - 'parent.classValue = {"age":31}', - 'child.classValue = {"age":22}', - 'parent.classValue = {"age":22}' + 'child.classValue = {"__age":31}', + 'parent.classValue = {"__age":31}', + 'child.classValue = {"__age":22}', + 'parent.classValue = {"__age":22}' ]) }) test("Two-way synchronization after changing attributes of Date", () => { testPageOnChange(control, "ProvideConsumeObject", [12, 22], [ "parent.date = 2025-4-1", - "parent.objectValue = {\"age\":18}", - "parent.classValue = {\"age\":18}", + "parent.objectValue = {\"__age\":18}", + "parent.classValue = {\"__age\":18}", "parent.unionValue = \"simple\"", "child.date = 2025-4-1", - "child.objectValue = {\"age\":18}", - "child.classValue = {\"age\":18}", + "child.objectValue = {\"__age\":18}", + "child.classValue = {\"__age\":18}", "child.unionValue = \"simple\"", "child.date = 2025-4-7", @@ -252,32 +252,32 @@ export function provideConsumeTests(control: AppControl) { }) // skip test as currently it is impossible to observe changes or class properties // waiting for ObjectProxy realization by panda runtime - test.expectFailure("Description of the problem", "Two-way synchronization after changing property of a class variable", () => { + test("Two-way synchronization after changing property of a class variable", () => { testPageOnChange(control, "ProvideConsumeObject", [15, 25], [ "parent.date = 2025-4-1", - "parent.objectValue = {\"age\":18}", - "parent.classValue = {\"age\":18}", + "parent.objectValue = {\"__age\":18}", + "parent.classValue = {\"__age\":18}", "parent.unionValue = \"simple\"", "child.date = 2025-4-1", - "child.objectValue = {\"age\":18}", - "child.classValue = {\"age\":18}", + "child.objectValue = {\"__age\":18}", + "child.classValue = {\"__age\":18}", "child.unionValue = \"simple\"", - "child.classValue = {\"age\":15}", - "parent.classValue = {\"age\":15}", - "child.classValue = {\"age\":25}", - "parent.classValue = {\"age\":25}", + "child.classValue = {\"__age\":15}", + "parent.classValue = {\"__age\":15}", + "child.classValue = {\"__age\":25}", + "parent.classValue = {\"__age\":25}", ]) }) test("Two-way synchronization of union variable", () => { testPageOnChange(control, "ProvideConsumeObject", [16, 17, 26], [ "parent.date = 2025-4-1", - "parent.objectValue = {\"age\":18}", - "parent.classValue = {\"age\":18}", + "parent.objectValue = {\"__age\":18}", + "parent.classValue = {\"__age\":18}", "parent.unionValue = \"simple\"", "child.date = 2025-4-1", - "child.objectValue = {\"age\":18}", - "child.classValue = {\"age\":18}", + "child.objectValue = {\"__age\":18}", + "child.classValue = {\"__age\":18}", "child.unionValue = \"simple\"", "child.unionValue = 125", diff --git a/ets-tests/ets/suites/StateManagement.ets b/ets-tests/ets/suites/StateManagement.ets index 6c8fc74d8153f7a4da2297e9c8abe5287c473c84..c451511350442d9051c832b6d4a81431b1cd0d34 100644 --- a/ets-tests/ets/suites/StateManagement.ets +++ b/ets-tests/ets/suites/StateManagement.ets @@ -42,6 +42,14 @@ function stateManagementTests(control: AppControl) { + "value = 20\n", "@State decorated variable was not changed correctly or there were no re-renders") }) + test("Change value of observable object", () => { + testPageOnChange(control, "ObservableObject", [11, 12, 13, 14], + "name = Alice, age = 3\n" + + "name = Alice, age = 4\n" + + "name = Bob, age = 4\n" + + "name = Alice, age = 5\n" + + "name = Alice, age = 6\n") + }) test("Change value of observable date", () => { testPageOnChange(control, "ObservableDate", [11, 12, 13, 14, 15, 16, 17], "date = 2020-2-29 17:0\n" diff --git a/incremental/compat/src/arkts/observable.ts b/incremental/compat/src/arkts/observable.ts index 81f0d7a1bdbc0e1d84293fe4c1d10c968e8acb57..5de4bbe7f1c8c93eebe1437d595852f402cde94a 100644 --- a/incremental/compat/src/arkts/observable.ts +++ b/incremental/compat/src/arkts/observable.ts @@ -171,6 +171,20 @@ export class ObservableHandler implements Observable { } return false } + + // TODO: onGet, onSet: temporary methods for observing object classes. Remove after fixing DefaultProxyHandler in ArkTS + static onGet(parent: T) { + const observable = ObservableHandler.find(parent) + if (observable) { + observable.onAccess() + } + } + static onSet(parent: T) { + const observable = ObservableHandler.find(parent) + if (observable) { + observable.onModify() + } + } } /** @internal */ @@ -210,9 +224,16 @@ export function observableProxy(value: Value, parent?: ObservableHandler, } else if (value instanceof Date) { return ObservableDate(value, parent, observed) as Value } - if (PROXY_DISABLED) return value as Value + // TODO: proxy the given object - return Proxy.create(value as Object, new CustomProxyHandler()) as Value + if (Type.of(value) instanceof ClassType) { + if (!PROXY_DISABLED) { + return Proxy.create(value as Object, new CustomProxyHandler()) as Value + } + ObservableHandler.installOn(value as Object, new ObservableHandler(parent)) + return value as Value + } + return value as Value } class CustomProxyHandler extends DefaultProxyHandler { @@ -231,6 +252,12 @@ class CustomProxyHandler extends DefaultProxyHandler { } return super.set(target, name, value) } + + override invoke(target: T, method: Method, args: FixedArray): NullishType { + const observable = ObservableHandler.find(target) + if (observable) observable.onAccess() + return super.invoke(target, method, args) + } } function proxyChildrenOnly(array: T[], parent: ObservableHandler, observed?: boolean) { diff --git a/ui2abc/ui-plugins/src/class-transformer.ts b/ui2abc/ui-plugins/src/class-transformer.ts new file mode 100644 index 0000000000000000000000000000000000000000..043e33878237302ff852c767404e6a174445bf7e --- /dev/null +++ b/ui2abc/ui-plugins/src/class-transformer.ts @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2022-2025 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. + */ + +import * as arkts from "@koalaui/libarkts" +import { Importer } from "./utils"; +import { generateThisBacking } from "./property-translators/utils"; +import { StructTable } from "./struct-recorder"; +import { Es2pandaTokenType } from "@koalaui/libarkts"; +import { ComponentStatementTransformer } from "./component-transformer"; + +export class ClassStatementTransformer implements ComponentStatementTransformer { + collectImports(imports: Importer): void { + // TODO: Temporary for observing object classes. Remove after fixing DefaultProxyHandler in ArkTS + imports.add('ObservableHandler', '@koalaui/common') + } + + check(statement: arkts.Statement): boolean { + return arkts.isClassDeclaration(statement) + } + + applyTransform(property: arkts.ClassDeclaration, result: arkts.Statement[]) { + result.push(this.rewriteCustomClass(property)) + } + + private rewriteCustomClass(clazz: arkts.ClassDeclaration): arkts.ClassDeclaration { + const classBody: arkts.AstNode[] = [] + clazz.definition?.body.forEach((element) => + classBody.push(new CustomClassBodyRewriter(classBody).visitor(element))) + + return arkts.factory.createClassDeclaration( + arkts.factory.createClassDefinition( + clazz.definition!.ident, + clazz.definition?.typeParams, + clazz.definition?.superTypeParams, + clazz.definition?.implements ?? [], + clazz.definition?.ctor, + clazz.definition?.super, + classBody, + clazz.definition?.modifiers!, + clazz.definition?.modifierFlags! + ) + ) + } +} + +class CustomClassBodyRewriter extends arkts.AbstractVisitor { + private readonly extraStmts: arkts.AstNode[] = [] + private isConstructorContext = false + + constructor(extra: arkts.AstNode[]) { + super() + this.extraStmts = extra + } + + visitor(node: arkts.AstNode): arkts.AstNode { + if (arkts.isMethodDefinition(node) && node.isConstructor) { + this.isConstructorContext = true + const result = this.visitEachChild(node) + this.isConstructorContext = false + return result + } else if (arkts.isClassProperty(node)) { + this.generateGetterSetter(node, this.extraStmts) + return this.rewriteClassProperty(node) + } else if (this.isConstructorContext + && arkts.isMemberExpression(node) + && arkts.isThisExpression(node.object) + && arkts.isIdentifier(node.property)) { + return generateThisBacking(this.getBackingPropName(node.property.name)) + } + + return this.visitEachChild(node) + } + + private getBackingPropName(propName: string): string { + return "__" + propName + } + + private rewriteClassProperty(property: arkts.ClassProperty) { + return arkts.factory.createClassProperty( + arkts.factory.createIdentifier(this.getBackingPropName(property.id?.name!)), + property.value, + property.typeAnnotation, + arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PRIVATE, + false + ) + } + + private createCallObservableHandler(methodName: "onGet" | "onSet"): arkts.ExpressionStatement { + return arkts.factory.createExpressionStatement( + arkts.factory.createCallExpression( + arkts.factory.createMemberExpression( + arkts.factory.createIdentifier("ObservableHandler"), + arkts.factory.createIdentifier(methodName), + arkts.Es2pandaMemberExpressionKind.MEMBER_EXPRESSION_KIND_PROPERTY_ACCESS, + false, + false + ), + [arkts.factory.createThisExpression()], + undefined + ) + ) + } + + private generateGetterSetter(property: arkts.ClassProperty, result: arkts.AstNode[]) { + const origPropName = property.id?.name! + const backingPropName = this.getBackingPropName(origPropName) + + const getterFunction = arkts.factory.createScriptFunction( + arkts.factory.createBlockStatement([ + this.createCallObservableHandler("onGet"), + arkts.factory.createReturnStatement(generateThisBacking(backingPropName)) + ]), + undefined, + [], + property.typeAnnotation, + true, + arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_METHOD + | arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_GETTER, + arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC, + arkts.factory.createIdentifier(origPropName), + [] + ) + + const setterFunction = arkts.factory.createScriptFunction( + arkts.factory.createBlockStatement([ + arkts.factory.createExpressionStatement( + arkts.factory.createAssignmentExpression( + generateThisBacking(backingPropName), + arkts.factory.createIdentifier("value"), + Es2pandaTokenType.TOKEN_TYPE_PUNCTUATOR_SUBSTITUTION + ) + ), + this.createCallObservableHandler("onSet"), + ]), + undefined, + [ + arkts.factory.createETSParameterExpression( + arkts.factory.createIdentifier("value"), + false, + undefined, + property.typeAnnotation! + ) + ], + undefined, + true, + arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_METHOD + | arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_SETTER + | arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_OVERLOAD, + arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC, + arkts.factory.createIdentifier(origPropName), + [] + ) + + const setter = arkts.factory.createMethodDefinition( + arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_SET, + arkts.factory.createIdentifier(origPropName), + arkts.factory.createFunctionExpression(arkts.factory.createIdentifier(origPropName), setterFunction), + arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC, + false + ) + + const getter = arkts.factory.createMethodDefinition( + arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_GET, + arkts.factory.createIdentifier(origPropName), + arkts.factory.createFunctionExpression(arkts.factory.createIdentifier(origPropName), getterFunction), + arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC, + false, + [setter] + ) + setter.parent = getter + result.push(getter) + } +} \ No newline at end of file diff --git a/ui2abc/ui-plugins/src/component-transformer.ts b/ui2abc/ui-plugins/src/component-transformer.ts index d90f6a6c43ba0a7c1dc78cc0b2fc233cb8f7618b..ebb68d0f6de9f9b000c167658210adb2da9389c1 100644 --- a/ui2abc/ui-plugins/src/component-transformer.ts +++ b/ui2abc/ui-plugins/src/component-transformer.ts @@ -18,14 +18,33 @@ import { CustomComponentNames, getCustomComponentOptionsName, Importer, - InternalAnnotations + InternalAnnotations, + ImportingTransformer } from "./utils"; -import { BuilderParamTransformer, ConsumeTransformer, LinkTransformer, LocalStorageLinkTransformer, LocalStoragePropTransformer, ObjectLinkTransformer, PropertyTransformer, PropTransformer, ProvideTransformer, StateTransformer, StorageLinkTransformer, StoragePropTransformer, PlainPropertyTransformer, fieldOf, isOptionBackedByProperty, isOptionBackedByPropertyName } from "./property-transformers"; +import { + BuilderParamTransformer, + ConsumeTransformer, + LinkTransformer, + LocalStorageLinkTransformer, + LocalStoragePropTransformer, + ObjectLinkTransformer, + PropertyTransformer, + PropTransformer, + ProvideTransformer, + StateTransformer, + StorageLinkTransformer, + StoragePropTransformer, + PlainPropertyTransformer, + fieldOf, + isOptionBackedByPropertyName +} from "./property-transformers"; import { annotation, isAnnotation } from "./common/arkts-utils"; import { DecoratorNames, DecoratorParameters, hasDecorator } from "./property-translators/utils"; import { factory } from "./ui-factory" +import { ClassStatementTransformer } from "./class-transformer"; +import { StructTable } from "./struct-recorder"; export interface ApplicationInfo { bundleName: string, @@ -37,6 +56,11 @@ export interface ComponentTransformerOptions { applicationInfo?: ApplicationInfo } +export interface ComponentStatementTransformer extends ImportingTransformer { + check(property: arkts.Statement): boolean + applyTransform(property: arkts.Statement, result: arkts.Statement[]): void +} + function computeOptionsName(clazz: arkts.ClassDeclaration): string { return clazz.definition?.typeParams?.params?.[1]?.name?.name ?? getCustomComponentOptionsName(clazz.definition?.ident?.name!) @@ -44,10 +68,14 @@ function computeOptionsName(clazz: arkts.ClassDeclaration): string { export class ComponentTransformer extends arkts.AbstractVisitor { private arkuiImport?: string + private statementsTransformers: ComponentStatementTransformer[] constructor(private imports: Importer, options?: ComponentTransformerOptions) { super() this.arkuiImport = options?.arkui + this.statementsTransformers = [ + new ClassStatementTransformer() + ] } private transformStatements(statements: readonly arkts.Statement[]): arkts.Statement[] { @@ -68,7 +96,13 @@ export class ComponentTransformer extends arkts.AbstractVisitor { if (arkts.isETSStructDeclaration(statement)) { this.rewriteStruct(statement, result) } else { - result.push(statement) + const transformer = this.statementsTransformers.find(it => it.check(statement)) + if (transformer) { + transformer.collectImports(this.imports) + transformer.applyTransform(statement, result) + } else { + result.push(statement) + } } }) return result @@ -414,4 +448,4 @@ function createVoidMethod(methodName: string, parameters: readonly arkts.Express arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PROTECTED, false ) -} +} \ No newline at end of file