Attribute Modifier (AttributeModifier)

Overview

The introduction of the @Styles and @Extend decorators in declarative syntax helps with reuse of custom styles, but they encounter limitations in certain scenarios:

  • Both @Styles and @Extend are processed at compile time and do not support cross-file exports for reuse.
  • @Styles only supports universal attributes and events, not component-specific attributes.
  • While @Styles allows for polymorphic styles, it does not support parameter passing, which means it cannot expose certain properties externally.
  • @Extend supports private attributes and events of specific components, but it does not support cross-file exports for reuse either.
  • Neither @Styles nor @Extend supports service logic for dynamically determining whether to set certain attributes. They only allow setting all possible attributes using ternary expressions, which is inefficient when dealing with a large number of attributes.

To address the above issues, ArkUI introduces the AttributeModifier mechanism, which allows for dynamic modification of attributes through Modifier objects. The table below is a comparison of the capabilities between the AttributeModifier mechanism and the @Styles and @Extend decorators.

Capability @Styles @Extend AttributeModifier
Cross-file export Not supported Not supported Supported
Universal attribute setting Supported Supported Supported
Universal event setting Supported Supported Partially supported
Component-specific attribute setting Not supported Supported Partially supported
Component-specific event setting Not supported Supported Partially supported
Parameter passing Not supported Supported Supported
Polymorphic styles Supported Not supported Supported
Service logic Not supported Not supported Supported

Clearly, when compared to @Styles and @Extend, AttributeModifier provides superior capabilities and flexibility. As it continues to evolve to encompass a full spectrum of attribute and event settings, AttributeModifier is the preferred choice for implementation.

API Definition

declare interface AttributeModifier<T> {

  applyNormalAttribute?(instance: T): void;

  applyPressedAttribute?(instance: T): void;

  applyFocusedAttribute?(instance: T): void;

  applyDisabledAttribute?(instance: T): void;

  applySelectedAttribute?(instance: T): void;

}

AttributeModifier is an API that requires you to implement methods in the form of applyXxxAttribute. Xxx signifies various states of polymorphism and can be in the following: Normal, Pressed, Focused, Disabled, and Selected. T represents the attribute type of the component. Within the callback, you can access the attribute object and use it to set the attributes.

declare class CommonMethod<T> {
  attributeModifier(modifier: AttributeModifier<T>): T;
}

attributeModifier is a universal component method that allows you to pass in a custom modifier. Since the type T is explicitly defined when a component is instantiated, the type T passed to the method must be the corresponding attribute type for that component, or it must be CommonAttribute.

How to Use

  • The attributeModifier method accepts an instance that implements the AttributeModifier<T> API. Here, T must be the specific attribute type corresponding to the component, or it must be CommonAttribute.
  • When a component is initialized for the first time or when its associated state variable changes, if the passed instance implements the corresponding API, the applyNormalAttribute callback will be invoked.
  • When the applyNormalAttribute callback is invoked, a component attribute object is passed in. Through this object, you can set the attributes and events of the current component.
  • If an attempt is made to execute attributes or events that are not yet supported, an exception will be thrown during execution.
  • When an attribute change triggers the applyXxxAttribute API, any attributes that were previously set on the component but not included in the current change will revert to their default values.
  • The API can be used to leverage polymorphic styling capabilities. For example, if you need to set certain attributes when the component enters a pressed state, you can implement the applyPressedAttribute method to achieve this.
  • If the same attribute is set on a component using both attribute methods and applyNormalAttribute, the principle of property override is followed, which means that the last set attributes take effect.
  • A single Modifier instance object can be used across multiple components.
  • If applyNormalAttribute is used multiple times on a single component with different Modifier instances, each time the state variables are updated, the attribute settings of these instances will be executed in the order they were applied, which also follows the principle of property override.

Setting and Modifying Component Attributes

AttributeModifier provides a powerful mechanism to separate the UI from styling. It enables the dynamic customization of component attributes with support for parameter passing and service logic writing, and triggers updates through state variables.

export class MyButtonModifier implements AttributeModifier<ButtonAttribute> {
  // A private member variable that can be dynamically modified externally
  public isDark: boolean = false

  // The constructor allows for parameter passing when creating an instance.
  constructor(dark?: boolean) {
    this.isDark = dark ?? false
  }

  applyNormalAttribute(instance: ButtonAttribute): void {
    // instance is the attribute object for the Button, which can be modified here.
    if (this.isDark) {// Service logic can be written here.
      // After attribute changes trigger the apply function, attributes that were set before but not included in the change will revert to their default values.
      instance.backgroundColor('#707070')
    } else {
      // Chaining of attribute methods is supported.
      instance.backgroundColor('#17A98D')
        .borderColor('#707070')
        .borderWidth(2)
    }
  }
}

// pages/Button1.ets
import { MyButtonModifier } from '../Common/ButtonModifier01'

@Entry
@Component
struct Button1 {
  // The modifier is decorated with @State, with behavior consistent with that of a regular object.
  @State modifier: MyButtonModifier = new MyButtonModifier(true);

  build() {
    Row() {
      Column() {
        Button('Button')
          .attributeModifier(this.modifier)
          .onClick(() => {
            // When the level-1 attribute of the modifier is changed, a UI update is triggered, causing applyNormalAttribute to be executed again.
            this.modifier.isDark = !this.modifier.isDark
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

AttributeModifier

If the same attribute is set on a component using both attribute methods and applyNormalAttribute, the principle of property override is followed, which means that the last set attributes take effect.

export class MyButtonModifier implements AttributeModifier<ButtonAttribute> {
  // A private member variable that can be dynamically modified externally
  public isDark: boolean = false

  // The constructor allows for parameter passing when creating an instance.
  constructor(dark?: boolean) {
    this.isDark = dark ?? false
  }

  applyNormalAttribute(instance: ButtonAttribute): void {
    // instance is the attribute object for the Button, which can be modified here.
    if (this.isDark) {// Service logic can be written here.
      // After attribute changes trigger the apply function, attributes that were set before but not included in the change will revert to their default values.
      instance.backgroundColor('#707070')
    } else {
      // Chaining of attribute methods is supported.
      instance.backgroundColor('#17A98D')
        .borderColor('#707070')
        .borderWidth(2)
    }
  }
}

// pages/Button2.ets
import { MyButtonModifier } from '../Common/ButtonModifier01'

@Entry
@Component
struct Button2 {
  @State modifier: MyButtonModifier = new MyButtonModifier(true);

  build() {
    Row() {
      Column() {
        // As the attribute is set before the modifier, the button's color changes in accordance with the value of the modifier.
        Button('Button')
          .backgroundColor('#2787D9')
          .attributeModifier(this.modifier)
          .onClick(() => {
            this.modifier.isDark = !this.modifier.isDark
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

AttributeModifier

If applyNormalAttribute is used multiple times on a single component with different Modifier instances, each time the state variables are updated, the attribute settings of these instances will be executed in the order they were applied, which also follows the principle of property override.

export class MyButtonModifier2 implements AttributeModifier<ButtonAttribute> {
  public isDark: boolean = false

  constructor(dark?: boolean) {
    this.isDark = dark ?? false
  }

  applyNormalAttribute(instance: ButtonAttribute): void {
    if (this.isDark) {
      instance.backgroundColor(Color.Black)
        .width(200)
    } else {
      instance.backgroundColor(Color.Red)
        .width(100)
    }
  }
}
export class MyButtonModifier3 implements AttributeModifier<ButtonAttribute> {
  public isDark2: boolean = false

  constructor(dark?: boolean) {
    this.isDark2 = dark ? dark : false
  }

  applyNormalAttribute(instance: ButtonAttribute): void {
    if (this.isDark2) {
      instance.backgroundColor('#2787D9')
    } else {
      instance.backgroundColor('#707070')
    }
  }
}

// pages/Button3.ets
import { MyButtonModifier2 } from '../Common/ButtonModifier02';
import { MyButtonModifier3 } from '../Common/ButtonModifier03';

@Entry
@Component
struct Button3 {
  @State modifier: MyButtonModifier2 = new MyButtonModifier2(true);
  @State modifier2: MyButtonModifier3 = new MyButtonModifier3(true);

  build() {
    Row() {
      Column() {
        Button('Button')
          .attributeModifier(this.modifier)
          .attributeModifier(this.modifier2)
          .onClick(() => {
            this.modifier.isDark = !this.modifier.isDark
            this.modifier2.isDark2 = !this.modifier2.isDark2
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}
![AttributeModifier](figures/AttributeModifier04.gif)

Setting Polymorphic Styles and Events

You can use AttributeModifier to set polymorphic styles and events, which enables the reuse of event logic and supports various states such as Normal, Pressed, Focused, Disabled, and Selected. For example, if you need to set certain attributes when the component enters a pressed state, you can implement the applyPressedAttribute method to achieve this.

export class MyButtonModifier4 implements AttributeModifier<ButtonAttribute> {
  applyNormalAttribute(instance: ButtonAttribute): void {
    // instance is the attribute object for the Button, used to set attributes for the normal state.
    instance.backgroundColor('#17A98D')
      .borderColor('#707070')
      .borderWidth(2)
  }

  applyPressedAttribute(instance: ButtonAttribute): void {
    // instance is the attribute object for the Button, used to set attributes for the pressed state.
    instance.backgroundColor('#2787D9')
      .borderColor('#FFC000')
      .borderWidth(5)
  }
}

// pages/Button4.ets
import { MyButtonModifier4 } from '../Common/ButtonModifier04'

@Entry
@Component
struct Button4 {
  @State modifier: MyButtonModifier4 = new MyButtonModifier4();

  build() {
    Row() {
      Column() {
        Button('Button')
          .attributeModifier(this.modifier)
      }
      .width('100%')
    }
    .height('100%')
  }
}
![AttributeModifier](figures/AttributeModifier02.gif)

attributeModifier Support for Attributes and Events

Dynamic attribute and event configuration via attributeModifier is supported since API version 11.

Attributes and Events Not Supporting attributeModifier

The following table lists attributes and events that currently do not support attributeModifier. Unless otherwise specified, attributes and events support attributeModifier by default from their initial release.

Component/Attribute Category Attribute/Event Name Error Message Description
CommonAttribute accessibilityText - -
CommonAttribute accessibilityDescription - -
CommonAttribute animation Method not implemented. Animation attributes are not supported.
CommonAttribute attributeModifier - attributeModifier does not take effect when nested.
CommonAttribute backgroundFilter is not callable -
CommonAttribute chainWeight is not callable -
CommonAttribute compositingFilter is not callable -
CommonAttribute drawModifier is not callable Modifier attributes are not supported.
CommonAttribute foregroundFilter is not callable -
CommonAttribute freeze is not callable -
CommonAttribute gesture Method not implemented. Gesture attributes are not supported.
CommonAttribute gestureModifier is not callable Modifier attributes are not supported.
CommonAttribute onAccessibilityHover is not callable -
CommonAttribute onDigitalCrown is not callable. -
CommonAttribute parallelGesture Method not implemented. Gesture attributes are not supported.
CommonAttribute priorityGesture Method not implemented. Gesture attributes are not supported.
CommonAttribute reuseId Method not implemented. -
CommonAttribute stateStyles Method not implemented. stateStyles attributes are not supported.
CommonAttribute useSizeType Method not implemented. Deprecated attributes are not supported.
CommonAttribute visualEffect is not callable -
CommonAttribute bindContextMenu Method not implemented. Attributes that accept a CustomBuilder are not supported.
CommonAttribute bindContentCover Method not implemented. Attributes that accept a CustomBuilder are not supported.
CommonAttribute bindSheet Method not implemented. Attributes that accept a CustomBuilder are not supported.
CommonAttribute dragPreview Builder is not supported. Attributes that accept a CustomBuilder are not supported.
CommonAttribute bindPopup Method not implemented. Attributes that accept a CustomBuilder are not supported.
CommonAttribute accessibilityVirtualNode is not callable Attributes that accept a CustomBuilder are not supported.
CommonAttribute chainWeight - -
CheckboxGroup contentModifier - -
CommonAttribute backgroundImage - -
CommonAttribute onClick - -
CommonAttribute toolbar - -
CommonAttribute accessibilityGroup - -
CommonAttribute reuse - -
CommonAttribute onGestureRecognizerJudgeBegin - -
EmbeddedComponent onError - -
EmbeddedComponent onTerminated - -
NavDestination backButtonIcon19+ - -
NavDestination menus19+ - -
NavDestination customTransition - -
Navigation backButtonIcon - -
Navigation menus - -
Repeat each - -
Repeat key - -
Repeat virtualScroll - -
Repeat template - -
Repeat templateId - -
Search customKeyboard - -
Search onWillAttachIME - -
Select menuItemContentModifier12+ - -
Select menuItemContentModifier18+ - -
Select textModifier - -
Select arrowModifier - -
Select optionTextModifier - -
Select selectedOptionTextModifier - -
Slider digitalCrownSensitivity - -
Swiper prevMargin - -
Swiper nextMargin - -
TextArea customKeyboard - -
Text bindSelectionMenu - -
TextInput customKeyboard - -
TextInput onWillAttachIME - -
TextPicker onEnterSelectedArea - -
TimePicker onEnterSelectedArea - -

Version Differences Between Attribute/Event Initial Release and attributeModifier Support

The following table shows cases where the initial release version of an attribute or event differs from its attributeModifier support version. Unless otherwise specified, attributes and events support attributeModifier by default from their initial release.

Component/Attribute Category Attribute/Event Name Initial Release Version attributeModifier Support Version
AlphabetIndexer autoCollapse 11 12
Button buttonStyle 11 12
Button controlSize 11 12
CalendarPicker onChange 18 20
Canvas enableAnalyzer 12 20
CommonAttribute accessibilityTextHint 12 20
CommonAttribute accessibilityChecked 13 20
CommonAttribute accessibilitySelected 13 20
CommonAttribute background 10 20
CommonAttribute visualEffect 12 20
CommonAttribute onDragStart 8 13
CommonAttribute onVisibleAreaApproximateChange 17 23
CommonAttribute onVisibleAreaChange 9 20
CommonAttribute onTouchIntercept 12 20
CommonAttribute onPreDrag 12 20
CommonAttribute onChildTouchTest 11 20
CommonAttribute backgroundFilter 12 20
CommonAttribute foregroundFilter 12 20
CommonAttribute compositingFilter 12 20
CommonAttribute foregroundBlurStyle 10 18
CommonAttribute freeze12+ 12 20
CommonAttribute freeze18+ 18 20
CommonAttribute dragPreviewOptions 11 12
CommonAttribute bindMenu 11 20
CommonAttribute transition 12 20
CommonAttribute safeAreaPadding 14 18
CommonAttribute pixelRound 11 12
ContainerSpan textBackgroundStyle 11 12
DatePicker onDateChange 18 20
FolderStack alignContent 11 12
FolderStack onFolderStateChange 11 20
FolderStack onHoverStatusChange 11 20
FolderStack enableAnimation 11 12
FolderStack autoHalfFold 11 12
Gauge privacySensitive 12 20
Image enableAnalyzer 11 12
Image resizable 11 20
List OnScrollVisibleContentChangeCallback 12 14
List onItemDragStart 8 14
NavDestination title 9 12
NavDestination mode 11 12
NavDestination backButtonIcon11+ 11 12
NavDestination menus12+ 12 14
NavDestination toolbarConfiguration 13 20
NavDestination onReady 11 20
NavDestination onWillAppear 12 20
NavDestination onWillDisappear 12 20
NavDestination onWillShow 12 20
NavDestination onWillHide 12 20
NavDestination systemBarStyle 12 20
NavDestination onResult 15 22
NavDestination bindToScrollable 14 22
NavDestination bindToNestedScrollable 14 22
NavDestination onActive 17 22
NavDestination onInactive 17 22
NavDestination onNewParam 19 22
Navigation title 8 12
Navigation toolbarConfiguration 10 20
Navigation customNavContentTransition 11 20
Navigation systemBarStyle 12 20
PatternLock backgroundColor 9 20
PatternLock onDotConnect 11 20
Progress privacySensitive 12 20
Refresh onOffsetChange 12 20
RichEditor customKeyboard 10 23
RichEditor onDidIMEInput 12 20
RichEditor enablePreviewText 12 18
RichEditor placeholder 12 18
RichEditor onWillChange 12 18
RichEditor onDidChange 12 18
RichEditor editMenuOptions 12 18
RichEditor enableKeyboardOnFocus 12 18
RichEditor enableHapticFeedback 13 20
RichEditor barState 13 18
Select menuBackgroundColor 11 12
Select menuBackgroundBlurStyle 11 12
Swiper displayCount 8 12
SymbolGlyph fontSize 11 12
SymbolGlyph fontColor 11 12
SymbolGlyph fontWeight 11 12
SymbolGlyph effectStrategy 11 12
SymbolGlyph renderingStrategy 11 12
SymbolSpan fontSize 11 12
SymbolSpan fontColor 11 12
SymbolSpan fontWeight 11 12
SymbolSpan effectStrategy 11 12
SymbolSpan renderingStrategy 11 12
ScrollableCommonAttribute onWillScroll 12 14
ScrollableCommonAttribute onDidScroll 12 14
TabContent onWillShow 12 20
TabContent onWillHide 12 20
Tabs edgeEffect 12 17
Tabs customContentTransition 11 20
Tabs onContentWillChange 12 20
Tabs barBackgroundBlurStyle 11 12
TextArea enterKeyType 11 12
Text enableHapticFeedback 13 18
TextInput showCounter 11 12
TextInput onSecurityStateChange 12 20
TextPicker onScrollStop14+ 14 20
TextPicker onScrollStop18+ 18 20
TextTimer textShadow 11 12
TimePicker enableHapticFeedback 12 18
TimePicker onChange 18 20
Video enableAnalyzer 12 20
Video analyzerConfig 12 20
Video onError 7 20
WaterFlow onScrollIndex 11 20