DEV Community

HarmonyOS
HarmonyOS

Posted on

Developing a JS Widget (FA Model)

Read the original article:Developing a JS Widget (FA Model)

Requirement Description

The Feature Ability (FA) model is supported since API version 7 but is no longer recommended for new development. The stage model is now the preferred choice for application development.

This document outlines the steps to develop a JS widget using the FA model, including lifecycle management, data persistence, UI development, and event handling.

Background Knowledge

Key Concepts

Widget Types:

Normal Widget: A widget designed for persistent use by the host system, typically placed on interfaces like a home screen and maintained until explicitly removed.

Temporary Widget: A widget intended for transient use, appearing briefly during specific interactions before disappearing.

Widget Lifecycle:

  • onCreate(): Called when the widget is created.
  • onUpdate(): Called when the widget is updated, either scheduled or periodically.
  • onDestroy(): Called when the widget is destroyed.

APIs

FormAbility APIs:

  • onCreate(want: Want): Called when the widget is created. Returns FormBindingData.
  • onCastToNormal(formId: string): Called when a temporary widget is converted to a normal one.
  • onUpdate(formId: string): Called when the widget is updated.
  • onVisibilityChange(newStatus: Record): Called when the widget's visibility changes.
  • onEvent(formId: string, message: string): Called when a widget event occurs.
  • onDestroy(formId: string): Called when the widget is destroyed.

FormProvider APIs:

  • setFormNextRefreshTime(formId: string, minute: number): Sets the next refresh time for a widget.
  • updateForm(formId: string, formBindingData: FormBindingData): Updates a widget's data.

Implementation Steps

Implement Widget Lifecycle Callbacks:

Define the LifeCycle class to implement all lifecycle callbacks.

Use the Want object to access parameters passed by the widget host.

Configure the Widget Configuration File:

Edit config.json to define the widget's configuration, including its type, update schedule, and other settings.

Persistently Store Widget Data:

Use dataPreferences to store and retrieve widget data.

Override onDestroy() to delete persistent data when the widget is destroyed.

Develop the Widget UI Page:

Use HML for the UI, CSS for styling, and JSON for data and event interactions.

Develop Widget Events:

Set onclick fields in HML to trigger router or message actions.

Code Snippet / Configuration

Implementing Widget Lifecycle Callbacks:

import type featureAbility from '@ohos.ability.featureAbility';
import type Want from '@ohos.app.ability.Want';
import formBindingData from '@ohos.app.form.formBindingData';
import formInfo from '@ohos.app.form.formInfo';
import formProvider from '@ohos.app.form.formProvider';
import dataPreferences from '@ohos.data.preferences';
import hilog from '@ohos.hilog';

const TAG: string = '[Sample_FAModelAbilityDevelop]';
const domain: number = 0xFF00;

const DATA_STORAGE_PATH: string = 'form_store';
let storeFormInfo = async (formId: string, formName: string, tempFlag: boolean, context: featureAbility.Context): Promise<void> => {
  // Only the widget ID (formId), widget name (formName), and whether the widget is a temporary one (tempFlag) are persistently stored.
  let formInfo: Record<string, string | number | boolean> = {
    'formName': 'formName',
    'tempFlag': 'tempFlag',
    'updateCount': 0
  };
  try {
    const storage = await dataPreferences.getPreferences(context, DATA_STORAGE_PATH);
    // Put the widget information.
    await storage.put(formId, JSON.stringify(formInfo));
    hilog.info(domain, TAG, `storeFormInfo, put form info successfully, formId: ${formId}`);
    await storage.flush();
  } catch (err) {
    hilog.error(domain, TAG, `failed to storeFormInfo, err: ${JSON.stringify(err as Error)}`);
  }
};

let deleteFormInfo = async (formId: string, context: featureAbility.Context) => {
  try {
    const storage = await dataPreferences.getPreferences(context, DATA_STORAGE_PATH);
    // Delete the widget information.
    await storage.delete(formId);
    hilog.info(domain, TAG, `deleteFormInfo, del form info successfully, formId: ${formId}`);
    await storage.flush();
  } catch (err) {
    hilog.error(domain, TAG, `failed to deleteFormInfo, err: ${JSON.stringify(err)}`);
  }
}

class LifeCycle {
  onCreate: (want: Want) => formBindingData.FormBindingData = (want) => ({ data: '' });
  onCastToNormal: (formId: string) => void = (formId) => {
  };
  onUpdate: (formId: string) => void = (formId) => {
  };
  onVisibilityChange: (newStatus: Record<string, number>) => void = (newStatus) => {
    let obj: Record<string, number> = {
      'test': 1
    };
    return obj;
  };
  onEvent: (formId: string, message: string) => void = (formId, message) => {
  };
  onDestroy: (formId: string) => void = (formId) => {
  };
  onAcquireFormState?: (want: Want) => formInfo.FormState = (want) => (0);
  onShareForm?: (formId: string) => Record<string, Object> = (formId) => {
    let obj: Record<string, number> = {
      'test': 1
    };
    return obj;
  };
}

let obj: LifeCycle = {
  onCreate(want: Want) {
    hilog.info(domain, TAG, 'FormAbility onCreate');
    if (want.parameters) {
      let formId = String(want.parameters['ohos.extra.param.key.form_identity']);
      let formName = String(want.parameters['ohos.extra.param.key.form_name']);
      let tempFlag = Boolean(want.parameters['ohos.extra.param.key.form_temporary']);
      hilog.info(domain, TAG, 'FormAbility onCreate' + formId);
      storeFormInfo(formId, formName, tempFlag, this.context);
    }

    let obj: Record<string, string> = {
      'title': 'titleOnCreate',
      'detail': 'detailOnCreate'
    };
    let formData: formBindingData.FormBindingData = formBindingData.createFormBindingData(obj);
    return formData;
  },
  onCastToNormal(formId: string) {
    hilog.info(domain, TAG, 'FormAbility onCastToNormal');
  },
  onUpdate(formId: string) {
    hilog.info(domain, TAG, 'FormAbility onUpdate');
    let obj: Record<string, string> = {
      'title': 'titleOnUpdate',
      'detail': 'detailOnUpdate'
    };
    let formData: formBindingData.FormBindingData = formBindingData.createFormBindingData(obj);
    formProvider.updateForm(formId, formData).catch((error: Error) => {
      hilog.error(domain, TAG, 'FormAbility updateForm, error:' + JSON.stringify(error));
    });
  },
  onVisibilityChange(newStatus: Record<string, number>) {
    hilog.info(domain, TAG, 'FormAbility onVisibilityChange');
  },
  onEvent(formId: string, message: string) {
    let obj: Record<string, string> = {
      'title': 'titleOnEvent',
      'detail': 'detailOnEvent'
    };
    let formData: formBindingData.FormBindingData = formBindingData.createFormBindingData(obj);
    formProvider.updateForm(formId, formData).catch((error: Error) => {
      hilog.error(domain, TAG, 'FormAbility updateForm, error:' + JSON.stringify(error));
    });
    hilog.info(domain, TAG, 'FormAbility onEvent');
  },
  onDestroy(formId: string) {
    hilog.info(domain, TAG, 'FormAbility onDestroy');
    deleteFormInfo(formId, this.context);
  },
  onAcquireFormState(want: Want) {
    hilog.info(domain, TAG, 'FormAbility onAcquireFormState');
    return formInfo.FormState.READY;
  }
};

export default obj;
Enter fullscreen mode Exit fullscreen mode

Configuring the Widget Configuration File (config.json)

"js": [
  {
    "name": "widget",
    "pages": [
      "pages/index/index"
    ],
    "window": {
      "designWidth": 720,
      "autoDesignWidth": true
    },
    "type": "form"
  }
]
Enter fullscreen mode Exit fullscreen mode

Configuring the Widget Configuration File (config.json) abilities

"abilities": [
  {
    "name": ".FormAbility",
    "srcPath": "FormAbility",
    "description": "$string:FormAbility_desc",
    "icon": "$media:icon",
    "label": "$string:FormAbility_label",
    "type": "service",
    "formsEnabled": true,
    "srcLanguage": "ets",
    "forms": [
      {
        "jsComponentName": "widget",
        "isDefault": true,
        "scheduledUpdateTime": "10:30",
        "defaultDimension": "2*2",
        "name": "widget",
        "description": "This is a service widget.",
        "colorMode": "auto",
        "type": "JS",
        "formVisibleNotify": true,
        "supportDimensions": [
          "2*2"
        ],
        "updateEnabled": true,
        "updateDuration": 1
      }
    ]
  }
]
Enter fullscreen mode Exit fullscreen mode

Persistently Storing Widget Data:

const TAG: string = '[Sample_FAModelAbilityDevelop]';
const domain: number = 0xFF00;

const DATA_STORAGE_PATH: string = 'form_store';
let storeFormInfo = async (formId: string, formName: string, tempFlag: boolean, context: featureAbility.Context): Promise<void> => {
  let formInfo: Record<string, string | number | boolean> = {
    'formName': 'formName',
    'tempFlag': 'tempFlag',
    'updateCount': 0
  };
  try {
    const storage = await dataPreferences.getPreferences(context, DATA_STORAGE_PATH);
    await storage.put(formId, JSON.stringify(formInfo));
    hilog.info(domain, TAG, `storeFormInfo, put form info successfully, formId: ${formId}`);
    await storage.flush();
  } catch (err) {
    hilog.error(domain, TAG, `failed to storeFormInfo, err: ${JSON.stringify(err as Error)}`);
  }
};

let deleteFormInfo = async (formId: string, context: featureAbility.Context) => {
  try {
    const storage = await dataPreferences.getPreferences(context, DATA_STORAGE_PATH);
    await storage.delete(formId);
    hilog.info(domain, TAG, `deleteFormInfo, del form info successfully, formId: ${formId}`);
    await storage.flush();
  } catch (err) {
    hilog.error(domain, TAG, `failed to deleteFormInfo, err: ${JSON.stringify(err)}`);
  }
}

// ...
onCreate(want: Want) {
  hilog.info(domain, TAG, 'FormAbility onCreate');
  if (want.parameters) {
    let formId = String(want.parameters['ohos.extra.param.key.form_identity']);
    let formName = String(want.parameters['ohos.extra.param.key.form_name']);
    let tempFlag = Boolean(want.parameters['ohos.extra.param.key.form_temporary']);
    hilog.info(domain, TAG, 'FormAbility onCreate' + formId);
    storeFormInfo(formId, formName, tempFlag, this.context);
  }

  let obj: Record<string, string> = {
    'title': 'titleOnCreate',
    'detail': 'detailOnCreate'
  };
  let formData: formBindingData.FormBindingData = formBindingData.createFormBindingData(obj);
  return formData;
},
// ...
Enter fullscreen mode Exit fullscreen mode

deleteFormInfo function usage

let deleteFormInfo = async (formId: string, context: featureAbility.Context): Promise<void> => {
  try {
    const storage = await dataPreferences.getPreferences(context, DATA_STORAGE_PATH);
    await storage.delete(formId);
    hilog.info(domain, TAG, `deleteFormInfo, del form info successfully, formId: ${formId}`);
    await storage.flush();
  } catch (err) {
    hilog.error(domain, TAG, `failed to deleteFormInfo, err: ${JSON.stringify(err)}`);
  }
};

// ...
onDestroy(formId: string) {
  hilog.info(domain, TAG, 'FormAbility onDestroy');
  deleteFormInfo(formId, this.context);
}
// ...
Enter fullscreen mode Exit fullscreen mode

Updating Widget Data:

const TAG: string = '[Sample_FAModelAbilityDevelop]';
const domain: number = 0xFF00;

onUpdate(formId: string) {
  hilog.info(domain, TAG, 'FormAbility onUpdate');
  let obj: Record<string, string> = {
    'title': 'titleOnUpdate',
    'detail': 'detailOnUpdate'
  };
  let formData: formBindingData.FormBindingData = formBindingData.createFormBindingData(obj);
  formProvider.updateForm(formId, formData).catch((error: Error) => {
    hilog.error(domain, TAG, 'FormAbility updateForm, error:' + JSON.stringify(error));
  });
}
Enter fullscreen mode Exit fullscreen mode

Developing the Widget UI Page:

HML:

<div class="container">
    <stack>
        <div class="container-img">
            <image src="/common/widget.png" class="bg-img"></image>
            <image src="/common/rect.png" class="bottom-img"></image>
        </div>
        <div class="container-inner">
            <text class="title" onclick="routerEvent">{{title}}</text>
            <text class="detail_text" onclick="messageEvent">{{detail}}</text>
        </div>
    </stack>
</div>
Enter fullscreen mode Exit fullscreen mode

CSS:

.container {
    flex-direction: column;
    justify-content: center;
    align-items: center;
}

.bg-img {
    flex-shrink: 0;
    height: 100%;
    z-index: 1;
}

.bottom-img {
    position: absolute;
    width: 150px;
    height: 56px;
    top: 63%;
    background-color: rgba(216, 216, 216, 0.15);
    filter: blur(20px);
    z-index: 2;
}

.container-inner {
    flex-direction: column;
    justify-content: flex-end;
    align-items: flex-start;
    height: 100%;
    width: 100%;
    padding: 12px;
}

.title {
    font-family: HarmonyHeiTi-Medium;
    font-size: 14px;
    color: rgba(255,255,255,0.90);
    letter-spacing: 0.6px;
}

.detail_text {
    font-family: HarmonyHeiTi;
    font-size: 12px;
    color: rgba(255,255,255,0.60);
    letter-spacing: 0.51px;
    text-overflow: ellipsis;
    max-lines: 1;
    margin-top: 6px;
}
Enter fullscreen mode Exit fullscreen mode

JSON:

{
  "data": {
    "title": "TitleDefault",
    "detail": "TextDefault"
  },
  "actions": {
    "routerEvent": {
      "action": "router",
      "abilityName": "com.samples.famodelabilitydevelop.MainAbility",
      "params": {
        "message": "add detail"
      }
    },
    "messageEvent": {
      "action": "message",
      "params": {
        "message": "add detail"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Developing Widget Events:

HML:

<div class="container">
    <stack>
        <div class="container-img">
            <image src="/common/widget.png" class="bg-img"></image>
            <image src="/common/rect.png" class="bottom-img"></image>
        </div>
        <div class="container-inner">
            <text class="title" onclick="routerEvent">{{title}}</text>
            <text class="detail_text" onclick="messageEvent">{{detail}}</text>
        </div>
    </stack>
</div>
Enter fullscreen mode Exit fullscreen mode

CSS:

.container {
    flex-direction: column;
    justify-content: center;
    align-items: center;
}

.bg-img {
    flex-shrink: 0;
    height: 100%;
    z-index: 1;
}

.bottom-img {
    position: absolute;
    width: 150px;
    height: 56px;
    top: 63%;
    background-color: rgba(216, 216, 216, 0.15);
    filter: blur(20px);
    z-index: 2;
}

.container-inner {
    flex-direction: column;
    justify-content: flex-end;
    align-items: flex-start;
    height: 100%;
    width: 100%;
    padding: 12px;
}

.title {
    font-family: HarmonyHeiTi-Medium;
    font-size: 14px;
    color: rgba(255,255,255,0.90);
    letter-spacing: 0.6px;
}

.detail_text {
    font-family: HarmonyHeiTi;
    font-size: 12px;
    color: rgba(255,255,255,0.60);
    letter-spacing: 0.51px;
    text-overflow: ellipsis;
    max-lines: 1;
    margin-top: 6px;
}
Enter fullscreen mode Exit fullscreen mode

JSON:

{
  "data": {
    "title": "TitleDefault",
    "detail": "TextDefault"
  },
  "actions": {
    "routerEvent": {
      "action": "router",
      "abilityName": "com.samples.famodelabilitydevelop.MainAbility",
      "params": {
        "message": "add detail"
      }
    },
    "messageEvent": {
      "action": "message",
      "params": {
        "message": "add detail"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Test Results

  • Widget Creation: Verify that onCreate() is called and data is stored correctly.
  • Scheduled Updates: Check that onUpdate() updates the widget at the specified time.
  • Data Persistence: Ensure widget data remains intact after framework restarts.
  • Event Handling: Test that router and message events trigger correctly.

Expected Result

  • Widgets are created, updated, and destroyed as per lifecycle callbacks.
  • Data is persisted using dataPreferences.
  • Events trigger router or message actions correctly.

Limitations or Considerations

  • Deprecation: The FA model is deprecated; the stage model is recommended for new development.
  • Temporary Widgets: Data for temporary widgets may be deleted if the framework restarts.

Related Documents or Links

  1. Application Data Persistence Overview
  2. FA Model Widget Documentation

Written by Emine INAN

Top comments (0)