Fabric Custom Component Development Guide
Custom Components of the ArkTS Version (only leaf node components are supported)
This section uses MarqueeView in the sample project as an example to describe how to implement a custom Fabric ArkTS component.
1. Write the code for RN to call the Fabric component
Write MarqueeViewNativeComponent.tsx. Note that if you want to use Codegen, the file must be named <ModuleName>NativeComponent. In the file, use codegenNativeComponent to create the MarqueeView component. The src attribute and onStop event are declared in MarqueeViewProps.
type OnStopEventData = Readonly<{
isStop: boolean
}>;
interface MarqueeViewProps extends ViewProps {
src: string,
onStop?: DirectEventHandler<OnStopEventData>;
}
const MarqueeView = codegenNativeComponent<MarqueeViewProps>(
'MarqueeView'
) as HostComponent<MarqueeViewProps>;
Add the MarqueeView tag to the component container in the same way as creating other standard components.
<MarqueeView
Src="Double 11 shopping festival. Consumption is an important and final link in the process of social reproduction. It refers to the process of using social products to meet people's various needs."
style={{height: 180, width: '100%', backgroundColor: 'hsl(210, 80%, 50%)'}}
onStop={(e) => {
SampleTurboModule.rnLog("Native calls onStop of RN, isStop = "+e.nativeEvent.isStop)
setMarqueeStop(e.nativeEvent.isStop)
}}
/>
2. Write Native ArkTS implementation code
Descriptor
Descriptor is used to encapsulate the parameters passed from the RN component code to the ArkUI component. MarqueeView provides a src parameter for RN to display the scrolling content. The code for defining MarqueeViewDescriptor on the native side is as follows:
export interface MarqueeViewProps extends ViewBaseProps {
src: string
}
export type MarqueeViewDescriptor = Descriptor<"MarqueeView", MarqueeViewProps>;
Descriptor is automatically generated by RNOH. The component tag does not need to be manually set. RNOH automatically allocates the component tag. You only need to use the getDescriptor method to obtain Descriptor of the corresponding tag.
this.descriptor = this.ctx.descriptorRegistry.getDescriptor<MarqueeViewDescriptor>(this.tag)
If the attribute parameters passed from the RN side change, Descriptor needs to be updated.
this.unregisterDescriptorChangesListener = this.ctx.descriptorRegistry.subscribeToDescriptorChanges(this.tag, (newDescriptor) => {
this.descriptor = (newDescriptor as MarqueeViewDescriptor)
})
RN calls the native component
RN calls UIManager.dispatchViewManagerCommand to send a message to the native component.
UIManager.dispatchViewManagerCommand(
findNodeHandle(nativeRef.current),
'toggleMarqueeState',
[],
)
The native component receives the message through componentCommandReceiver.registerCommandCallback and executes the corresponding method.
this.ctx.componentCommandReceiver.registerCommandCallback(this.tag, (commandName) => {
if (commandName === "toggleMarqueeState") {
this.start = !this.start
console.log("will emitComponentEvent");
}
})
Native component calls RN
Add the onStop method on the RN side:
<MarqueeView
...
onStop={(e) => {
// The native component calls the onStop method of MarqueeView on the RN side.
const isStop = e.nativeEvent.isStop
...
}}
/>
The native component sends a message for calling the RN component event:
this.ctx.rnInstance.emitComponentEvent(
this.descriptor.tag,
"MarqueeView",
{ type: "onStop", isStop: !this.start }
)
arkTsComponentNames
When creating an RNApp or RNInstance, place the component name in the array and pass it as a parameter of arkTsComponentNames.
const arkTsComponentNames = [MarqueeView.NAME];
// Used as a parameter of RNApp.
RNApp({
rnInstanceConfig: {
···
arkTsComponentNames: arkTsComponentNames
},
···
})
// Passed in when creating an RNInstance.
await rnohCoreContext.createAndRegisterRNInstance({
···
arkTsComponentNames: arkTsComponentNames
});
buildCustomComponent
When a RNSurface is created to load a JSBundle, buildCustomComponent is passed in to load the native Fabric component.
import { RNAbility, ComponentBuilderContext, RNSurface } from "rnoh";
import { MarqueeView } from '../customView/MarqueeView'
@Builder
public buildCustomComponent(ctx: ComponentBuilderContext) {
if (ctx.componentName === MarqueeView.NAME) {
MarqueeView({
ctx: ctx.rnComponentContext,
tag: ctx.tag
})
}
}
...
RNSurface({
...
buildCustomComponent: this.buildCustomComponent,
})
3. Write C++ code of Codegen
You can use Codegen to generate the glue code for C++ or manually implement the code. However, Codegen is recommended. This section describes how to manually implement the code.
- Create a C++ class for
PropsandEmitter, and bind the C++ class toDescriptor. - Implement the
handleEventmethod ofMarqueeViewEventEmitRequestHandler, and calleventEmitterto send an event message to the RN component based on the event name of native message. - Implement the
MarqueeViewJSIBinderclass forPropsandEmitterbinding. - Implement the
MarqueeViewNapiBinderclass for property mapping. - Import the preceding files to the corresponding method implementation of
SampleTurboModulePackagefor binding.
Props
Create a C++ file for Props to define the Descriptor properties of MarqueeView.
Props.h:
#include <jsi/jsi.h>
#include <react/renderer/components/view/ViewProps.h>
#include <react/renderer/core/PropsParserContext.h>
#include <react/debug/react_native_assert.h>
namespace facebook {
namespace react {
class JSI_EXPORT MarqueeViewProps final : public ViewProps {
public:
MarqueeViewProps() = default;
MarqueeViewProps(const PropsParserContext &context, const MarqueeViewProps &sourceProps, const RawProps &rawProps);
#pragma mark - Props
std::string src{""};
};
} // namespace react
} // namespace facebook
// Props.cpp
#include <react/renderer/components/rncore/Props.h>
#include <react/renderer/core/PropsParserContext.h>
#include <react/renderer/core/propsConversions.h>
#include "Props.h"
namespace facebook {
namespace react {
MarqueeViewProps::MarqueeViewProps(
const PropsParserContext &context,
const MarqueeViewProps &sourceProps,
const RawProps &rawProps): ViewProps(context, sourceProps, rawProps),
src(convertRawProp(context, rawProps, "src", sourceProps.src, {""}))
{}
} // namespace react
} // namespace facebook
MarqueeViewEventEmitter
Add the onStop method to MarqueeViewEventEmitter.h, with a custom property struct.
#include <react/renderer/components/view/ViewEventEmitter.h>
#include <jsi/jsi.h>
namespace facebook {
namespace react {
class JSI_EXPORT MarqueeViewEventEmitter : public ViewEventEmitter {
public:
using ViewEventEmitter::ViewEventEmitter;
struct OnStop {
bool isStop;
};
void onStop(OnStop value) const;
};
} // namespace react
} // namespace facebook
The sending and parameter binding of onStop are implemented in MarqueeViewEventEmitter.cpp.
#include "MarqueeViewEventEmitter.h"
namespace facebook {
namespace react {
void MarqueeViewEventEmitter::onStop(OnStop event) const {
dispatchEvent("stop", [event = std::move(event)](jsi::Runtime &runtime) {
auto payload = jsi::Object(runtime);
payload.setProperty(runtime, "isStop", event.isStop);
return payload;
});
}
} // namespace react
} // namespace facebook
MarqueeViewComponentDescriptor.h
Bind MarqueeViewProps and MarqueeViewEventEmitter to MarqueeViewComponentDescriptor.
#include <react/renderer/core/ConcreteComponentDescriptor.h>
#include <react/renderer/components/view/ConcreteViewShadowNode.h>
#include <react/renderer/components/view/ViewShadowNode.h>
#include "MarqueeViewEventEmitter.h"
#include "Props.h"
namespace facebook {
namespace react {
extern const char MarqueeViewComponentName[] = "MarqueeView";
using MarqueeViewShadowNode = ConcreteViewShadowNode<MarqueeViewComponentName, MarqueeViewProps, MarqueeViewEventEmitter>;
using MarqueeViewComponentDescriptor = ConcreteComponentDescriptor<MarqueeViewShadowNode>;
} // namespace react
} // namespace facebook
MarqueeViewEventEmitRequestHandler
In the handleEvent method, call the event message sending method eventEmitter->onStop(event) based on the event name.
class MarqueeViewEventEmitRequestHandler : public EventEmitRequestHandler {
public:
void handleEvent(EventEmitRequestHandler::Context const &ctx) override {
if (ctx.eventName != "MarqueeView") {
return;
}
ArkJS arkJs(ctx.env);
auto eventEmitter = ctx.shadowViewRegistry->getEventEmitter<react::MarqueeViewEventEmitter>(ctx.tag);
if (eventEmitter == nullptr) {
return;
}
MarqueeViewEventType type = getMarqueeViewEventType(arkJs, ctx.payload);
switch (type) {
case MarqueeViewEventType::MARQUEE_VIEW_ON_STOP: {
bool isStop = (bool)arkJs.getBoolean(arkJs.getObjectProperty(ctx.payload, "isStop"));
react::MarqueeViewEventEmitter::OnStop event{isStop};
eventEmitter->onStop(event);
break;
}
default:
break;
}
};
};
MarqueeViewJSIBinder
JSIBinder implements RN properties and methods at the JSI layer. It mainly calls the object.setProperty(rt, "src", "string") and events.setProperty(rt, "topStop", createDirectEvent(rt, "onStop")) methods. Pay attention to the naming rules of topStop and onStop in events.setProperty.
#pragma once
#include "RNOHCorePackage/ComponentBinders/ViewComponentJSIBinder.h"
namespace rnoh {
class MarqueeViewJSIBinder : public ViewComponentJSIBinder {
facebook::jsi::Object createNativeProps(facebook::jsi::Runtime &rt) override {
auto object = ViewComponentJSIBinder::createNativeProps(rt);
object.setProperty(rt, "src", "string");
return object;
}
facebook::jsi::Object createDirectEventTypes(facebook::jsi::Runtime &rt) override {
facebook::jsi::Object events(rt);
events.setProperty(rt, "topStop", createDirectEvent(rt, "onStop"));
return events;
}
};
} // namespace rnoh
NapiBinder
NapiBinder implements the property mapping between the C++ code and native component code. The src field is added to props whose .addProperty("src", props->src) is MarqueeViewDescriptor. If the code is not added, MarqueeView needs to obtain src from rawProps.
#include "RNOHCorePackage/ComponentBinders/ViewComponentNapiBinder.h"
#include "Props.h"
namespace rnoh {
class MarqueeViewNapiBinder : public ViewComponentNapiBinder {
public:
napi_value createProps(napi_env env, facebook::react::ShadowView const shadowView) override {
napi_value napiViewProps = ViewComponentNapiBinder::createProps(env, shadowView);
if (auto props = std::dynamic_pointer_cast<const facebook::react::MarqueeViewProps>(shadowView.props)) {
return ArkJS(env)
.getObjectBuilder(napiViewProps)
.addProperty("src", props->src)
.build();
}
return napiViewProps;
};
};
} // namespace rnoh
SampleTurboModulePackage
Add the following method declaration related to the custom component to SampleTurboModulePackage.h:
#include "RNOH/Package.h"
namespace rnoh {
class SampleTurboModulePackage : public Package {
public:
std::vector<facebook::react::ComponentDescriptorProvider> createComponentDescriptorProviders() override;
ComponentNapiBinderByString createComponentNapiBinderByName() override;
ComponentJSIBinderByString createComponentJSIBinderByName() override;
EventEmitRequestHandlers createEventEmitRequestHandlers() override;
};
} // namespace rnoh
Use MarqueeViewComponentDescriptor, MarqueeViewEventEmitRequestHandler, MarqueeViewNapiBinder, and MarqueeViewJSIBinder to implement the corresponding methods in SampleTurboModulePackage.cpp.
std::vector<react::ComponentDescriptorProvider> SampleTurboModulePackage::createComponentDescriptorProviders() {
return {
react::concreteComponentDescriptorProvider<react::MarqueeViewComponentDescriptor>(),
};
}
EventEmitRequestHandlers SampleTurboModulePackage::createEventEmitRequestHandlers() {
return {std::make_shared<MarqueeViewEventEmitRequestHandler>()};
}
ComponentNapiBinderByString SampleTurboModulePackage::createComponentNapiBinderByName() {
return {{"MarqueeView", std::make_shared<MarqueeViewNapiBinder>()}};
};
ComponentJSIBinderByString SampleTurboModulePackage::createComponentJSIBinderByName() {
return {{"MarqueeView", std::make_shared<MarqueeViewJSIBinder>()}};
};
4. Optimize native ArkTS components
In the preceding ArkTS component implementation, the attributes are set by calling the corresponding APIs. This implementation mode has the following disadvantages:
- Low execution efficiency caused by too many custom component attributes. To use the complete system component attributes, you need to register each attribute value in the encapsulated custom component. This greatly affects the build efficiency of each component.
- Difficult to maintain: When the system component attributes in a custom component are changed, the custom component also needs to be adapted.
To address the preceding disadvantages, ArkTS provides the attribute modifier for each system component, including the attributeModifier method. This method separates the component attribute settings to the AttributeModifier API implementation class instance provided by the system. The AttributeModifier API is implemented by customizing the class to extend the system component attributes. This section describes how to optimize the ArkTS component.
export class MarqueeModifier implements AttributeModifier<MarqueeAttribute> {
private constructor() {}
private static instance: MarqueeModifier;
protected descriptor: ViewBaseDescriptor = {} as ViewBaseDescriptor;
// Provide a singleton method to obtain the MarqueeModifier instance.
public static getInstance(): MarqueeModifier {
if (!MarqueeModifier.instance) {
MarqueeModifier.instance = new MarqueeModifier ();
}
return MarqueeModifier.instance;
}
// Provide a method to set the description of the component. The attributes and events to be registered for the component instance can be obtained by parsing the description.
setDescriptor(descriptor: ViewBaseDescriptor): MarqueeModifier {
this.descriptor = descriptor;
return MarqueeModifier.instance;
}
// API method called by ArkUI to complete the final MarqueeAttribute operation.
applyNormalAttribute(instance: MarqueeAttribute): void {
instance.width(this.descriptor.layoutMetrics.frame.size.width);
instance.height(this.descriptor.layoutMetrics.frame.size.height);
instance.position({ y: this.descriptor.layoutMetrics.frame.origin.y, x: this.descriptor.layoutMetrics.frame.origin.x });
if (this.descriptor.props.backgroundColor) {
instance.backgroundColor(this.descriptor.props.backgroundColor);
}
/* ...... Other attributes to be set*/
}
}
@Builder
export function marqueeBuilder(ctx: RNOHContext, descriptor: ViewBaseDescriptor) {
Marquee(···) {
// Use the AttributeModifier method to dynamically obtain the attributes and events to be registered for component instantiation.
.attributeModifier(MarqueeModifier.getInstance().setDescriptor(descriptor))
}
}
How do I create a custom CAPI component?
To create a Fabric component, you need to implement the following code:
- ComponentInstance: logic set of a custom component and the main part that needs to be implemented by a custom component. That is, one custom component file corresponds to one
ComponentInstance. - Package: creation declaration of a custom component. When an
Instanceis created, the correspondingInstanceobject is generated based on the name in thePackagefile. - Props: props parameter declaration of
ComponentInstance, used to pass JS parameters. In a custom component, props is also required when the parent component sends data to the child component. - EventEmitter: event declaration of
ComponentInstance, used to obtain the event callback set by the frontend and trigger the event at a proper time in the component. - ShadowNode: a class that needs to be declared when a
ComponentInstanceis created. It combines the correspondingProps,EventEmitter, andComponentName(name of the createdComponentInstance).
1. Write the code for RN to call the Fabric component
This section uses ButtonView in the sample project as an example to describe how to implement the Fabric CAPI custom component.
Write ButtonViewNativeComponent.tsx. Note that if you want to use Codegen, the file must be named <ModuleName>NativeComponent. In the file, use codegenNativeComponent to create the ButtonView component. The buttonText attribute and onButtonClick event are declared in ButtonViewProps.
export type OnButtonClickEventData = Readonly<{
isButtonClick: boolean,
type: string,
}>;
export interface ButtonViewProps extends ViewProps {
buttonText: string,
onButtonClick?: DirectEventHandler<OnButtonClickEventData>;
}
export default codegenNativeComponent<ButtonViewProps>(
'CustomButtonView',
) as HostComponent<ButtonViewProps>;
Add the ButtonView tag to the component container in the same way as creating other standard components.
<ButtonView
buttonText={"ButtonView: " + (buttonClick ? 'Click' : 'No Click')}
ref={nativeRefButton}
style= {{height: 50}}
onButtonClick={(e) => {
setButtonClick(e.nativeEvent.isButtonClick);
}}
/>
2. Write native C-API implementation code
C-API Component Structure
A native custom component consists of two parts: ButtonViewComponentInstance.cpp and ButtonViewNode.cpp. Specifically, ButtonViewComponentInstance is the native instance corresponding to the ButtonView component on the JS side. It inherits the component instance template class CppComponentInstance in RN and converts it to the ShadowNode type implemented by the component. It needs to override some common methods in the base class, including onChildInserted, getLocalRootArkUINode, and onPropsChanged. It can be used to implement component-specific methods. In addition, it inherits the delegate class ButtonViewNodeDelegate declared in ButtonViewNode and overrides the onXXX event in the delegate class to report events.
CppComponentInstance is the parent class of the ComponentInstance class of all custom components. All components inherit from this class, which contains basic methods for performing operations on components.
// ButtonViewComponentInstance.h
#include "RNOH/CppComponentInstance.h"
#include "ButtonViewNode.h"
#include "ButtonViewComponentDescriptor.h"
...
namespace rnoh {
class ButtonViewComponentInstance : public CppComponentInstance<facebook::react::ButtonViewShadowNode>, public ButtonViewNodeDelegate {
private:
ButtonViewNode m_buttonViewNode;
...
public:
// Override common methods in CppComponentInstance.
ButtonViewComponentInstance(Context context);
void onChildInserted(ComponentInstance::Shared const &childComponentInstance, std::size_t index) override;
void onChildRemoved(ComponentInstance::Shared const &childComponentInstance) override;
ButtonViewNode &getLocalRootArkUINode() override;
void onPropsChanged(SharedConcreteProps const &props) override;
// Handle the command.
void handleCommand(std::string const &commandName, folly::dynamic const &args) override;
...
// Override the event report method of ButtonViewNodeDelegate.
void onButtonClick() override;
...
};
} // namespace rnoh
ButtonViewNode corresponds to a specific OpenHarmony native component node and is held by a component instance as a member. When an attribute is set or a command is handled, ButtonViewComponentInstance calls the specific method implemented on ButtonViewNode. Take the buttonText attribute exposed by ButtonView on the RN side as an example. When the attribute is set or updated on the JS side, the framework calls the onPropsChanged method of ButtonViewComponentInstance. In this method, the setLabel method of ButtonViewNode is called to set resources.
#include "RNOH/arkui/ArkUINode.h"
#include "RNOH/arkui/NativeNodeApi.h"
...
namespace rnoh {
// Declare the delegate class.
class ButtonViewNodeDelegate {
public:
virtual ~ButtonViewNodeDelegate() = default;
// Declare the event API of the component.
virtual void onButtonClick(){};
};
class ButtonNode : public ArkUINode {
protected:
ButtonNodeDelegate* m_buttonNodeDelegate;
public:
ButtonNode();
~ButtonNode();
// Implement the common methods of the node.
void insertChild(ArkUINode& child, std::size_t index);
void removeChild(ArkUINode& child);
void onNodeEvent(ArkUI_NodeEventType eventType, EventArgs& eventArgs) override;
ButtonNode& setButtonNodeDelegate(ButtonNodeDelegate* buttonNodeDelegate);
···
// Implement the attribute setting method of the component.
ButtonNode& setLabel(const std::string &src);
};
} // namespace rnoh
Attributes, Commands, and Events
Attributes
Attributes set on the RN side are passed to ComponentInstance through props. The changed Props can be obtained by using onPropsChanged in ComponentInstance. The diff judgment needs to be performed in onPropsChanged, and then the attribute setting methods implemented by the ComponentNode object stored in ComponentInstance can be called.
// ButtonViewComponentInstance.cpp
void ButtonViewComponentInstance::onPropsChanged(
SharedConcreteProps const& props) {
CppComponentInstance::onPropsChanged(props);
if (!m_props || props->buttonText != m_props->buttonText) {
m_buttonNode.setLabel(props->buttonText);
}
}
The corresponding attributes need to be defined and set in ComponentNode.
// ButtonNode.cpp
ButtonNode& ButtonNode::setLabel(const std::string &src) {
ArkUI_AttributeItem labelItem = {.string = src.c_str()};
maybeThrow(NativeNodeApi::getInstance()->setAttribute(
m_nodeHandle, NODE_BUTTON_LABEL, &labelItem, this));
return *this;
}
Commands
RN needs to call UIManager.dispatchViewManagerCommand to send messages to the native system.
UIManager.dispatchViewManagerCommand(
findNodeHandle(nativeRef.current),
'changeButtonText',
['changeButtonText'],
)
The command channel has been encapsulated in the RN framework. The native end only needs to override the handleCommand method at the instance layer and call the corresponding method implemented at the node layer based on the passed command name. The arguments are recorded in args as objects.
void ButtonViewComponentInstance::handleCommand(
std::string const& commandName,
folly::dynamic const& args) {
if (commandName == "changeButtonText") {
m_buttonNode.setLabel(args[0].asString());
}
}
Events
The onButtonClick event callback emitter is added on the RN side.
<ButtonView
···
onButtonClick={(e) => {
// The native component calls the onButtonClick method of ButtonView on the RN side.
setButtonClick(e.nativeEvent.isButtonClick);
}}
/>
The native end overrides the onButtonClick method in the event delegate class at the instance layer and calls the corresponding ButtonViewEventEmitter.h method to pass the event to RN.
void ButtonViewComponentInstance::onButtonClick() {
facebook::react::ButtonViewEventEmitter::OnButtonClick m_onButtonClick;
m_onButtonClick.isButtonClick = true;
m_onButtonClick.type = "custom";
m_eventEmitter->onButtonClick(m_onButtonClick);
}
The event receiving entry is in the corresponding node class. When constructing the node class, you need to register the event to be listened on by the component and override the onNodeEvent method. In this method, the specific event method implemented at the instance layer is called.
// Enumerate the event listened by the component.
static constexpr ArkUI_NodeEventType Button_NODE_EVENT_TYPES[] = {
NODE_ON_CLICK};
···
// Register an event emitter when constructing a node.
ButtonNode::ButtonNode()
: ArkUINode(NativeNodeApi::getInstance()->createNode(
ArkUI_NodeType::ARKUI_NODE_BUTTON)) {
for (auto eventType : Button_NODE_EVENT_TYPES) {
maybeThrow(NativeNodeApi::getInstance()->registerNodeEvent(
m_nodeHandle, eventType, eventType, this));
}
}
···
// Override the onNodeEvent report event.
void ButtonNode::onNodeEvent(
ArkUI_NodeEventType eventType,
EventArgs& eventArgs) {
if (eventType == ArkUI_NodeEventType::NODE_ON_CLICK) {
if (m_buttonNodeDelegate != nullptr) {
m_buttonNodeDelegate->onButtonClick();
}
}
}
3. Write C++ code of Codegen
You can use Codegen to generate the glue code for C++ or manually implement the code. However, Codegen is recommended. This section describes how to manually implement the code.
- Create a C++ class for
PropsandEmitter, bind the class toDescriptor, and register theNodetype. - Implement the
ButtonViewJSIBinderclass for Props and Emitter binding. - Import the preceding files to the corresponding method implementation of
SampleTurboModulePackagefor binding.
Props
Create the C++ file of Props to define the attributes of ButtonView.
Props.h:
#pragma once
#include <jsi/jsi.h>
#include <react/renderer/components/view/ViewProps.h>
#include <react/renderer/core/PropsParserContext.h>
#include <react/debug/react_native_assert.h>
namespace facebook {
namespace react {
class JSI_EXPORT ButtonViewProps final : public ViewProps {
public:
ButtonViewProps() = default;
ButtonViewProps(const PropsParserContext &context, const ButtonViewProps &sourceProps, const RawProps &rawProps);
#pragma mark - Props
std::string buttonText{""};
};
} // namespace react
} // namespace facebook
// Props.cpp
#include <react/renderer/components/rncore/Props.h>
#include <react/renderer/core/PropsParserContext.h>
#include <react/renderer/core/propsConversions.h>
#include "Props.h"
namespace facebook {
namespace react {
ButtonViewProps::ButtonViewProps(
const PropsParserContext &context,
const ButtonViewProps &sourceProps,
const RawProps &rawProps): ViewProps(context, sourceProps, rawProps),
buttonText(convertRawProp(context, rawProps, "buttonText", sourceProps.buttonText, {""}))
{}
} // namespace react
} // namespace facebook
ButtonViewEventEmitter
Add the onButtonClick method to ButtonViewEventEmitter.h, with a custom property struct.
#pragma once
#include <react/renderer/components/view/ViewEventEmitter.h>
#include <jsi/jsi.h>
namespace facebook {
namespace react {
class JSI_EXPORT ButtonViewEventEmitter : public ViewEventEmitter {
public:
using ViewEventEmitter::ViewEventEmitter;
struct OnButtonClick {
bool isButtonClick;
std::string type;
};
void onButtonClick(OnButtonClick value) const;
};
} // namespace react
} // namespace facebook
The sending and parameter binding of onButtonClick are implemented in ButtonViewEventEmitter.cpp.
#include "ButtonViewEventEmitter.h"
namespace facebook {
namespace react {
void ButtonViewEventEmitter::onButtonClick(OnButtonClick event) const {
dispatchEvent("topButtonClick", [event = std::move(event)](jsi::Runtime &runtime) {
auto payload = jsi::Object(runtime);
payload.setProperty(runtime, "isButtonClick", event.isButtonClick);
payload.setProperty(runtime, "type", event.type);
return payload;
});
}
} // namespace react
} // namespace facebook
ButtonViewComponentDescriptor.h
Bind ButtonViewProps and ButtonViewEventEmitter to ButtonViewShadowNode.
#include <react/renderer/core/ConcreteComponentDescriptor.h>
#include <react/renderer/components/view/ConcreteViewShadowNode.h>
#include <react/renderer/components/view/ViewShadowNode.h>
#include "ButtonViewEventEmitter.h"
#include "Props.h"
namespace facebook {
namespace react {
const char ButtonViewComponentName[] = "ButtonView";
using ButtonViewShadowNode = ConcreteViewShadowNode<ButtonViewComponentName, ButtonViewProps, ButtonViewEventEmitter>;
using ButtonViewComponentDescriptor = ConcreteComponentDescriptor<ButtonViewShadowNode>;
} // namespace react
} // namespace facebook
ButtonViewJSIBinder
JSIBinder implements RN properties and methods at the JSI layer. It mainly calls the object.setProperty(rt, "buttonText", "string") and events.setProperty(rt, "topButtonClick", createDirectEvent(rt, "onButtonClick")); methods. Pay attention to the naming rules of topButtonClick and onButtonClick in events.setProperty.
#pragma once
#include "RNOHCorePackage/ComponentBinders/ViewComponentJSIBinder.h"
namespace rnoh {
class ButtonViewJSIBinder : public ViewComponentJSIBinder {
facebook::jsi::Object createNativeProps(facebook::jsi::Runtime &rt) override {
auto object = ViewComponentJSIBinder::createNativeProps(rt);
object.setProperty(rt, "buttonText", "string");
return object;
}
facebook::jsi::Object createDirectEventTypes(facebook::jsi::Runtime &rt) override {
facebook::jsi::Object events(rt);
events.setProperty(rt, "topButtonClick", createDirectEvent(rt, "onButtonClick"));
return events;
}
};
} // namespace rnoh
SampleTurboModulePackage
Add the following method declaration related to the custom component to SampleTurboModulePackage.h:
#include "RNOH/Package.h"
namespace rnoh {
class SampleTurboModulePackage : public Package {
public:
ComponentInstanceFactoryDelegate::Shared createComponentInstanceFactoryDelegate() override;
ComponentJSIBinderByString createComponentJSIBinderByName() override;
// Common methods used in the ArkTS version
···
};
} // namespace rnoh
Create a ButtonViewPackageComponentInstanceFactoryDelegate object and use ButtonViewPackageComponentInstanceFactoryDelegate, ButtonViewJSIBinder, and ButtonViewComponentInstance to implement the corresponding methods in SampleTurboModulePackage.cpp.
class ButtonViewPackageComponentInstanceFactoryDelegate : public ComponentInstanceFactoryDelegate {
public:
using ComponentInstanceFactoryDelegate::ComponentInstanceFactoryDelegate;
ComponentInstance::Shared create(ComponentInstance::Context ctx) override {
if (ctx.componentName == "CustomButtonView") {
return std::make_shared<ButtonViewComponentInstance>(std::move(ctx));
}
return nullptr;
}
};
···
ComponentJSIBinderByString SampleTurboModulePackage::createComponentJSIBinderByName() {
return {
{"MarqueeView", std::make_shared<MarqueeViewJSIBinder>(),},
{"ButtonView", std::make_shared<ButtonViewJSIBinder>()}
};
};
ComponentInstanceFactoryDelegate::Shared SampleTurboModulePackage::createComponentInstanceFactoryDelegate() override {
return std::make_shared<ButtonViewPackageComponentInstanceFactoryDelegate>();
}
···
4. Use other custom components in a custom component
A custom component may not be implemented by a base component. Instead, it is implemented by a combination of multiple base components. For example, a custom Scroll can contain a Header component. In the C-API version, there are two methods to achieve this effect:
- Method 1: The
ComponentInstanceof each component contains the node object of each component. The node provides various operations such as modifying attributes and sending events. Therefore, you can record multiple node objects in the sameComponentInstance, override thegetLocalRootArkUINode()method to obtain container nodes, override theonChildInserted()method to insert subnodes into the container class of the component, and add the logic for processing multiple nodes in theComponentInstance. - Method 2: All components exist in the form of
ComponentInstance. In this case, theComponentInstanceof the parent component can hold theComponentInstanceof the child component to form a new component. You need to add the corresponding method of the child component to theonPropsChangedandhandleCommandmethods of the parent component.
Parent-Child Component Communication
Parent-to-Child
A parent component has the child component itself or the corresponding Delegate. The parent component directly passes parameters to the child component by calling .setProps of the child component, or passes small-scale parameters by using Delegate.
For example, set the imageUrl address of the child component.
mainImgComponentInstance = std::dynamic_pointer_cast<FlowMainImgComponentInstance>(componentInstance);
m_mainImgDelegate = std::dynamic_popinter_cast<rnoh::MainImgDelegate>(mainImgComponentInstance);
if (mainImgComponentInstance) {
this->onChildInserted(mainImgComponentInstance, mainImgIndex);
m_mainImgDelegate->setImage(imgUrl);
}
Child-to-Parent
-
The type and parameter must be defined in the child component. The parameter is used to store the callback of the parent component.
public: using onDeleteCardCallback = std::function<void(std::unordered_map<std::string, folly::dynamic> &&)>; ... std::unordered_map<std::string, folly::dynamic> m_waterFlowInfo; onDeleteCardCallback m_onCardDeleteChanged; -
A value assignment API is defined in the child component.
void FlowPicTxtCardComponentInstance::setOnCardDelete(onDeleteCardCallback callback) { m_onCardDeleteChanged = std::move(callback) } -
The parent component calls the value assignment API of the child component to assign a value to the parameter of the child component.
flowPicTxtCardComponentInstance->setOnCardDelete([this]( std::map<std::string, folly::dynamic> &&expoInfo) { }); -
The callback in the child component is called to pass the parameter information to the parent component.
void FlowPicTxtCardComponentInstance::onTextClick(ArkUI_NodeEvent *event) { if (m_onCardDeleteChanged) { m_onCardDeleteChanged(std::move(m_waterFlowInfo)); } }