1# \@Observed and \@ObjectLink Decorators: Observing Attribute Changes in Nested Class Objects 2 3 4The decorators described above can observe only the changes of the first layer. However, in real-world application development, the application may encapsulate its own data model based on development requirements. In the case of multi-layer nesting, for example, a two-dimensional array, an array item class, or a class insider another class as an attribute, the attribute changes at the second layer cannot be observed. This is where the \@Observed and \@ObjectLink decorators come in handy. 5 6 7> **NOTE** 8> 9> Since API version 9, these two decorators are supported in ArkTS widgets. 10 11 12## Overview 13 14\@ObjectLink and \@Observed class decorators are used for two-way data synchronization in scenarios involving nested objects or arrays: 15 16- Regarding classes decorated by \@Observed, the attribute changes can be observed. 17 18- The \@ObjectLink decorated state variable in the child component is used to accept the instance of the \@Observed decorated class and establish two-way data binding with the corresponding state variable in the parent component. The instance can be an \@Observed decorated item in the array or an \@Observed decorated attribute in the class object. 19 20- Using \@Observed alone has no effect. Combined use with \@ObjectLink for two-way synchronization or with [\@Prop](arkts-prop.md) for one-way synchronization is required. 21 22 23## Restrictions 24 25Using \@Observed to decorate a class changes the original prototype chain of the class. Using \@Observed and other class decorators to decorate the same class may cause problems. 26 27## Decorator Description 28 29| \@Observed Decorator| Description | 30| -------------- | --------------------------------- | 31| Decorator parameters | None. | 32| Class decorator | Decorates a class. You must use **new** to create a class object before defining the class.| 33 34| \@ObjectLink Decorator| Description | 35| ----------------- | ---------------------------------------- | 36| Decorator parameters | None. | 37| Synchronization type | No synchronization with the parent component. | 38| Allowed variable types | Objects of \@Observed decorated classes. The type must be specified.<br>Simple type variables are not supported. Use [\@Prop](arkts-prop.md) instead.<br>Instances of classes that inherit **Date** or **Array** are supported. For details, see [Observed Changes](#observed-changes).<br>An \@ObjectLink decorated variable accepts changes to its attributes, but assignment is not allowed. In other words, an \@ObjectLink decorated variable is read-only and cannot be changed.| 39| Initial value for the decorated variable | Not allowed. | 40 41Example of a read-only \@ObjectLink decorated variable: 42 43 44```ts 45// The \@ObjectLink decorated variable accepts changes to its attribute. 46this.objLink.a= ... 47// Value assignment is not allowed for the \@ObjectLink decorated variable. 48this.objLink= ... 49``` 50 51> **NOTE** 52> 53> Value assignment is not allowed for the \@ObjectLink decorated variable. To assign a value, use [@Prop](arkts-prop.md) instead. 54> 55> - \@Prop creates a one-way synchronization from the data source to the decorated variable. It takes a copy of its source tp enable changes to remain local. When \@Prop observes a change to its source, the local value of the \@Prop decorated variable is overwritten. 56> 57> - \@ObjectLink creates a two-way synchronization between the data source and the decorated variable. An \@ObjectLink decorated variable can be considered as a pointer to the source object inside the parent component. Do not assign values to \@ObjectLink decorated variables, as doing so will interrupt the synchronization chain. \@ObjectLink decorated variables are initialized through data source (Object) references. Assigning a value to them is equivalent to updating the array item or class attribute in the parent component, which is not supported in TypeScript/JavaScript and will result in a runtime error. 58 59 60## Variable Transfer/Access Rules 61 62| \@ObjectLink Transfer/Access| Description | 63| ----------------- | ---------------------------------------- | 64| Initialization from the parent component | Mandatory.<br>To initialize an \@ObjectLink decorated variable, a variable in the parent component must meet all the following conditions:<br>- The variable type is an \@Observed decorated class.<br>- The initialized value must be an array item or a class attribute.<br>- The class or array of the synchronization source must be decorated by \@State, \@Link, \@Provide, \@Consume, or \@ObjectLink.<br>For an example where the synchronization source is an array item, see [Object Array](#object-array). For an example of the initialized class, see [Nested Object](#nested-object).| 65| Synchronize with the source | Two-way. | 66| Subnode initialization | Supported; can be used to initialize a regular variable or \@State, \@Link, \@Prop, or \@Provide decorated variable in the child component.| 67 68 69 **Figure 1** Initialization rule 70 71 72 73 74 75## Observed Changes and Behavior 76 77 78### Observed Changes 79 80If the attribute of an \@Observed decorated class is not of the simple type, such as class, object, or array, it must be decorated by \@Observed. Otherwise, the attribute changes cannot be observed. 81 82 83```ts 84class ClassA { 85 public c: number; 86 87 constructor(c: number) { 88 this.c = c; 89 } 90} 91 92@Observed 93class ClassB { 94 public a: ClassA; 95 public b: number; 96 97 constructor(a: ClassA, b: number) { 98 this.a = a; 99 this.b = b; 100 } 101} 102``` 103 104In the preceding example, **ClassB** is decorated by \@Observed, and the value changes of its member variables can be observed. In contrast, **ClassA** is not decorated by \@Observed, and therefore its attribute changes cannot be observed. 105 106 107```ts 108@ObjectLink b: ClassB 109 110// The value assignment can be observed. 111this.b.a = new ClassA(5) 112this.b.b = 5 113 114// ClassA is not decorated by @Observed, and its attribute changes cannot be observed. 115this.b.a.c = 5 116``` 117 118\@ObjectLink: \@ObjectLink can only accept instances of classes decorated by \@Observed. The following can be observed: 119 120- Value changes of the attributes that **Object.keys(observedObject)** returns. For details, see [Nested Object](#nested-object). 121 122- Replacement of array items for the data source of an array and changes of class attributes for the data source of a class. For details, see [Object Array](#object-array). 123 124For an instance of the class that inherits **Date**, the value assignment of **Date** can be observed. In addition, you can call the following APIs to update the attributes of **Date**: setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds. 125 126```ts 127@Observed 128class DateClass extends Date { 129 constructor(args: number | string) { 130 super(args) 131 } 132} 133 134@Observed 135class ClassB { 136 public a: DateClass; 137 138 constructor(a: DateClass) { 139 this.a = a; 140 } 141} 142 143@Component 144struct ViewA { 145 label: string = 'date'; 146 @ObjectLink a: DateClass; 147 148 build() { 149 Column() { 150 Button(`child increase the day by 1`) 151 .onClick(() => { 152 this.a.setDate(this.a.getDate() + 1); 153 }) 154 DatePicker({ 155 start: new Date('1970-1-1'), 156 end: new Date('2100-1-1'), 157 selected: this.a 158 }) 159 } 160 } 161} 162 163@Entry 164@Component 165struct ViewB { 166 @State b: ClassB = new ClassB(new DateClass('2023-1-1')); 167 168 build() { 169 Column() { 170 ViewA({ label: 'date', a: this.b.a }) 171 172 Button(`parent update the new date`) 173 .onClick(() => { 174 this.b.a = new DateClass('2023-07-07'); 175 }) 176 Button(`ViewB: this.b = new ClassB(new DateClass('2023-08-20'))`) 177 .onClick(() => { 178 this.b = new ClassB(new DateClass('2023-08-20')); 179 }) 180 } 181 } 182} 183``` 184 185 186### Framework Behavior 187 1881. Initial render: 189 1. \@Observed causes all instances of the decorated class to be wrapped with an opaque proxy object, which takes over the setter and getter methods of the attributes on the class. 190 2. The \@ObjectLink decorated variable in the child component is initialized from the parent component and accepts the instance of the \@Observed decorated class. The \@ObjectLink decorated wrapped object registers itself with the \@Observed decorated class. 191 1922. Attribute update: When the attribute of the \@Observed decorated class is updated, the system uses the setter and getter of the proxy, traverses the \@ObjectLink decorated wrapped objects that depend on it, and notifies the data update. 193 194 195## Application Scenarios 196 197 198### Nested Object 199 200The following is the data structure of a nested class object. 201 202 203```ts 204// objectLinkNestedObjects.ets 205let NextID: number = 1; 206 207@Observed 208class ClassA { 209 public id: number; 210 public c: number; 211 212 constructor(c: number) { 213 this.id = NextID++; 214 this.c = c; 215 } 216} 217 218@Observed 219class ClassB { 220 public a: ClassA; 221 222 constructor(a: ClassA) { 223 this.a = a; 224 } 225} 226 227@Observed 228class ClassD { 229 public c: ClassC; 230 231 constructor(c: ClassC) { 232 this.c = c; 233 } 234} 235 236@Observed 237class ClassC extends ClassA { 238 public k: number; 239 240 constructor(k: number) { 241 // Invoke the parent class method to process k. 242 super(k); 243 this.k = k; 244 } 245} 246``` 247 248 249 The following component hierarchy presents this data structure. 250 251```ts 252@Component 253struct ViewC { 254 label: string = 'ViewC1'; 255 @ObjectLink c: ClassC; 256 257 build() { 258 Row() { 259 Column() { 260 Text(`ViewC [${this.label}] this.a.c = ${this.c.c}`) 261 .fontColor('#ffffffff') 262 .backgroundColor('#ff3fc4c4') 263 .height(50) 264 .borderRadius(25) 265 Button(`ViewC: this.c.c add 1`) 266 .backgroundColor('#ff7fcf58') 267 .onClick(() => { 268 this.c.c += 1; 269 console.log('this.c.c:' + this.c.c) 270 }) 271 } 272 .width(300) 273 } 274} 275} 276 277@Entry 278@Component 279struct ViewB { 280 @State b: ClassB = new ClassB(new ClassA(0)); 281 @State child : ClassD = new ClassD(new ClassC(0)); 282 build() { 283 Column() { 284 ViewC({ label: 'ViewC #3', c: this.child.c}) 285 Button(`ViewC: this.child.c.c add 10`) 286 .backgroundColor('#ff7fcf58') 287 .onClick(() => { 288 this.child.c.c += 10 289 console.log('this.child.c.c:' + this.child.c.c) 290 }) 291 } 292 } 293} 294``` 295 296The @Observed decorated **ClassC** class can observe changes in attributes inherited from the base class. 297 298 299Event handlers in **ViewB**: 300 301 302- this.child.c = new ClassA(0) and this.b = new ClassB(new ClassA(0)): Change to the \@State decorated variable **b** and its attributes. 303 304- this.child.c.c = ... : Change at the second layer. Though [@State](arkts-state.md#observed-changes) cannot observe the change at the second layer, the change of an attribute of \@Observed decorated ClassA, which is attribute **c** in this example, can be observed by \@ObjectLink. 305 306 307Event handle in **ViewC**: 308 309 310- this.c.c += 1: Changes to the \@ObjectLink decorated variable **a** cause the button label to be updated. Unlike \@Prop, \@ObjectLink does not have a copy of its source. Instead, \@ObjectLink creates a reference to its source. 311 312- The \@ObjectLink decorated variable is read-only. Assigning **this.a = new ClassA(...)** is not allowed. Once value assignment occurs, the reference to the data source is reset and the synchronization is interrupted. 313 314 315### Object Array 316 317An object array is a frequently used data structure. The following example shows the usage of array objects. 318 319 320```ts 321@Component 322struct ViewA { 323 // The type of @ObjectLink of the child component ViewA is ClassA. 324 @ObjectLink a: ClassA; 325 label: string = 'ViewA1'; 326 327 build() { 328 Row() { 329 Button(`ViewA [${this.label}] this.a.c = ${this.a.c} +1`) 330 .onClick(() => { 331 this.a.c += 1; 332 }) 333 } 334 } 335} 336 337@Entry 338@Component 339struct ViewB { 340 // ViewB has the @State decorated ClassA[]. 341 @State arrA: ClassA[] = [new ClassA(0), new ClassA(0)]; 342 343 build() { 344 Column() { 345 ForEach(this.arrA, 346 (item: ClassA) => { 347 ViewA({ label: `#${item.id}`, a: item }) 348 }, 349 (item: ClassA): string => item.id.toString() 350 ) 351 // Initialize the @ObjectLink decorated variable using the array item in the @State decorated array, which is an instance of ClassA decorated by @Observed. 352 ViewA({ label: `ViewA this.arrA[first]`, a: this.arrA[0] }) 353 ViewA({ label: `ViewA this.arrA[last]`, a: this.arrA[this.arrA.length-1] }) 354 355 Button(`ViewB: reset array`) 356 .onClick(() => { 357 this.arrA = [new ClassA(0), new ClassA(0)]; 358 }) 359 Button(`ViewB: push`) 360 .onClick(() => { 361 this.arrA.push(new ClassA(0)) 362 }) 363 Button(`ViewB: shift`) 364 .onClick(() => { 365 this.arrA.shift() 366 }) 367 Button(`ViewB: chg item property in middle`) 368 .onClick(() => { 369 this.arrA[Math.floor(this.arrA.length / 2)].c = 10; 370 }) 371 Button(`ViewB: chg item property in middle`) 372 .onClick(() => { 373 this.arrA[Math.floor(this.arrA.length / 2)] = new ClassA(11); 374 }) 375 } 376 } 377} 378``` 379 380- this.arrA[Math.floor(this.arrA.length/2)] = new ClassA(..): The change of this state variable triggers two updates. 381 1. ForEach: The value assignment of the array item causes the change of [itemGenerator](arkts-rendering-control-foreach.md#api-description) of **ForEach**. Therefore, the array item is identified as changed, and the item builder of ForEach is executed to create a **ViewA** component instance. 382 2. ViewA({ label: ViewA this.arrA[first], a: this.arrA[0] }): The preceding update changes the first element in the array. Therefore, the **ViewA** component instance bound to **this.arrA[0]** is updated. 383 384- this.arrA.push(new ClassA(0)): The change of this state variable triggers two updates with different effects. 385 1. ForEach: The newly added **ClassA** object is unknown to the **ForEach** [itemGenerator](arkts-rendering-control-foreach.md#api-description). The item builder of **ForEach** will be executed to create a **ViewA** component instance. 386 2. ViewA({ label: ViewA this.arrA[last], a: this.arrA[this.arrA.length-1] }): The last item of the array is changed. As a result, the second **View A** component instance is changed. For **ViewA({ label: ViewA this.arrA[first], a: this.arrA[0] })**, a change to the array does not trigger a change to the array item, so the first **View A** component instance is not refreshed. 387 388- this.arrA[Math.floor (this.arrA.length/2)].c: [@State](arkts-state.md#observed-changes) cannot observe changes at the second layer. However, as **ClassA** is decorated by \@Observed, the change of its attributes will be observed by \@ObjectLink. 389 390 391### Two-Dimensional Array 392 393@Observed class decoration is required for a two-dimensional array. You can declare an \@Observed decorated class that extends from **Array**. 394 395 396```ts 397@Observed 398class StringArray extends Array<String> { 399} 400``` 401 402 403 404Declare a class that extends from **Array**: **class StringArray extends Array\<String> {}** and create an instance of **StringArray**. The use of the **new** operator is required for the \@Observed class decorator to work properly. 405 406 407```ts 408@Observed 409class StringArray extends Array<String> { 410} 411 412@Component 413struct ItemPage { 414 @ObjectLink itemArr: StringArray; 415 416 build() { 417 Row() { 418 Text('ItemPage') 419 .width(100).height(100) 420 421 ForEach(this.itemArr, 422 (item: string | Resource) => { 423 Text(item) 424 .width(100).height(100) 425 }, 426 (item: string) => item 427 ) 428 } 429 } 430} 431 432@Entry 433@Component 434struct IndexPage { 435 @State arr: Array<StringArray> = [new StringArray(), new StringArray(), new StringArray()]; 436 437 build() { 438 Column() { 439 ItemPage({ itemArr: this.arr[0] }) 440 ItemPage({ itemArr: this.arr[1] }) 441 ItemPage({ itemArr: this.arr[2] }) 442 Divider() 443 444 445 ForEach(this.arr, 446 (itemArr: StringArray) => { 447 ItemPage({ itemArr: itemArr }) 448 }, 449 (itemArr: string) => itemArr[0] 450 ) 451 452 Divider() 453 454 Button('update') 455 .onClick(() => { 456 console.error('Update all items in arr'); 457 if ((this.arr[0] as Array<String>)[0] !== undefined) { 458 // We should have a real ID to use with ForEach, but we do no. 459 // Therefore, we need to make sure the pushed strings are unique. 460 this.arr[0].push(`${this.arr[0].slice(-1).pop()}${this.arr[0].slice(-1).pop()}`); 461 this.arr[1].push(`${this.arr[1].slice(-1).pop()}${this.arr[1].slice(-1).pop()}`); 462 this.arr[2].push(`${this.arr[2].slice(-1).pop()}${this.arr[2].slice(-1).pop()}`); 463 } else { 464 this.arr[0].push('Hello'); 465 this.arr[1].push('World'); 466 this.arr[2].push('!'); 467 } 468 }) 469 } 470 } 471} 472``` 473