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;
Configuring the Widget Configuration File (config.json)
"js": [
{
"name": "widget",
"pages": [
"pages/index/index"
],
"window": {
"designWidth": 720,
"autoDesignWidth": true
},
"type": "form"
}
]
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
}
]
}
]
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;
},
// ...
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);
}
// ...
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));
});
}
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>
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;
}
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"
}
}
}
}
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>
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;
}
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"
}
}
}
}
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.
Top comments (0)