• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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![en-us_image_0000001502255262](figures/en-us_image_0000001502255262.png)
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