Custom Element

If the built-in elements do not meet your requirements, you can extend Lynx's capabilities by creating custom native elements. This section will guide you through creating and registering custom elements on Android and iOS platforms.

Prerequisites

✅ Completed Quick Start

✅ Completed Lynx Integration

✅ Familiar with element Basics

Building your Native Code

The implementation of custom native elements can be broken down into several steps, including: declaring and registering elements, creating native views, handling styles and properties, event binding, etc. Let's take a simple custom input element <input> as an example to briefly introduce the implementation process of custom elements.

The complete implementation can be found in the LynxExplorer/input module. You can compile and run the LynxExplorer sample project to preview element behavior in real-time.

Declare and Register Elements

Declare Custom Elements

A declared custom element needs to inherit from LynxUI. Below is the implementation of the <input> element:

LynxExplorerInput.h

#import <Lynx/LynxUI.h>

NS_ASSUME_NONNULL_BEGIN

@interface LynxTextField : UITextField

@property(nonatomic, assign) UIEdgeInsets padding;

@end

@interface LynxExplorerInput : LynxUI <LynxTextField *> <UITextFieldDelegate>

@end

NS_ASSUME_NONNULL_END
LynxExplorerInput.m

#import "LynxExplorerInput.h"

@implementation LynxExplorerInput

//...

@end



@implementation LynxTextField

- (UIEditingInteractionConfiguration)editingInteractionConfiguration API_AVAILABLE(ios(13.0)) {
  return UIEditingInteractionConfigurationNone;
}

- (void)setPadding:(UIEdgeInsets)padding {
  _padding = padding;
  [self setNeedsLayout];
}

- (CGRect)textRectForBounds:(CGRect)bounds {
  CGFloat x = self.padding.left;
  CGFloat y = self.padding.top;
  CGFloat width = bounds.size.width - self.padding.left - self.padding.right;
  CGFloat height = bounds.size.height - self.padding.top - self.padding.bottom;

  return CGRectMake(x, y, width, height);
}

- (CGRect)editingRectForBounds:(CGRect)bounds {
  return [self textRectForBounds:bounds];
}
@end

Register Custom Element

Elements can be registered in two ways: globally and locally.

Global Registration

Globally registered elements can be shared across multiple LynxView instances.

LynxExplorerInput.m

#import "LynxExplorerInput.h"
#import <Lynx/LynxComponentRegistry.h>

@implementation LynxExplorerInput

LYNX_LAZY_REGISTER_UI("input")

@end



@implementation LynxTextField

- (UIEditingInteractionConfiguration)editingInteractionConfiguration API_AVAILABLE(ios(13.0)) {
  return UIEditingInteractionConfigurationNone;
}

- (void)setPadding:(UIEdgeInsets)padding {
  _padding = padding;
  [self setNeedsLayout];
}

- (CGRect)textRectForBounds:(CGRect)bounds {
  CGFloat x = self.padding.left;
  CGFloat y = self.padding.top;
  CGFloat width = bounds.size.width - self.padding.left - self.padding.right;
  CGFloat height = bounds.size.height - self.padding.top - self.padding.bottom;

  return CGRectMake(x, y, width, height);
}

- (CGRect)editingRectForBounds:(CGRect)bounds {
  return [self textRectForBounds:bounds];
}
@end
Local Registration

Locally registered elements are only applicable to the current LynxView instance.

#import <Lynx/LynxEnv.h>
#import <Lynx/LynxView.h>

  LynxView *lynxView = [[LynxView alloc] initWithBuilderBlock:^(LynxViewBuilder *builder) {
    builder.config =
        [[LynxConfig alloc] initWithProvider:[LynxEnv sharedInstance].config.templateProvider];
    [builder.config registerUI:[LynxExplorerInput class] withName:@"input"];
  }];

Where "input" corresponds to the tag name in the front-end DSL. When Lynx Engine parses this tag, it will look for the registered native element and create an instance.

Create NativeView Instance

Each custom element needs to implement the createView method, which returns a corresponding native View instance.

Here is the implementation for the <input> element:

LynxExplorerInput.m
#import "LynxExplorerInput.h"
#import <Lynx/LynxComponentRegistry.h>

@implementation LynxExplorerInput

LYNX_LAZY_REGISTER_UI("input")

- (UITextField *)createView {
  UITextField *textField = [[LynxTextField alloc] init];
  //...
  textField.delegate = self;
  return textField;
}

@end

@implementation LynxTextField

- (UIEditingInteractionConfiguration)editingInteractionConfiguration API_AVAILABLE(ios(13.0)) {
  return UIEditingInteractionConfigurationNone;
}

- (void)setPadding:(UIEdgeInsets)padding {
  _padding = padding;
  [self setNeedsLayout];
}

- (CGRect)textRectForBounds:(CGRect)bounds {
  CGFloat x = self.padding.left;
  CGFloat y = self.padding.top;
  CGFloat width = bounds.size.width - self.padding.left - self.padding.right;
  CGFloat height = bounds.size.height - self.padding.top - self.padding.bottom;

  return CGRectMake(x, y, width, height);
}

- (CGRect)editingRectForBounds:(CGRect)bounds {
  return [self textRectForBounds:bounds];
}
@end

Handle Front-End Style and Property Updates

You can use the LYNX_PROP_SETTER macro to listen for property changes passed from the front end and update the native view. For example, handling the value property of the <input> element:

LynxExplorerInput.m

#import "LynxExplorerInput.h"
#import <Lynx/LynxComponentRegistry.h>
#import <Lynx/LynxPropsProcessor.h>

@implementation LynxExplorerInput

LYNX_LAZY_REGISTER_UI("input")

LYNX_PROP_SETTER("value", setValue, NSString *) {
    self.view.text = value;
}

- (UITextField *)createView {
  UITextField *textField = [[LynxTextField alloc] init];
  //...
  textField.delegate = self;
  return textField;
}

@end

@implementation LynxTextField

- (UIEditingInteractionConfiguration)editingInteractionConfiguration API_AVAILABLE(ios(13.0)) {
  return UIEditingInteractionConfigurationNone;
}

- (void)setPadding:(UIEdgeInsets)padding {
  _padding = padding;
  [self setNeedsLayout];
}

- (CGRect)textRectForBounds:(CGRect)bounds {
  CGFloat x = self.padding.left;
  CGFloat y = self.padding.top;
  CGFloat width = bounds.size.width - self.padding.left - self.padding.right;
  CGFloat height = bounds.size.height - self.padding.top - self.padding.bottom;

  return CGRectMake(x, y, width, height);
}

- (CGRect)editingRectForBounds:(CGRect)bounds {
  return [self textRectForBounds:bounds];
}
@end

Handle Layout Information (Optional)

Handle Lynx Engine Layout Results

Typically, Lynx Engine automatically calculates and updates the View layout information, so developers do not need to manually handle this. However, in some special cases, such as when additional adjustments to the View are required, you can obtain the latest layout information in the layoutDidFinished callback and apply custom logic.

LynxExplorerInput.m
#import "LynxExplorerInput.h"
#import <Lynx/LynxComponentRegistry.h>
#import <Lynx/LynxPropsProcessor.h>

@implementation LynxExplorerInput

LYNX_LAZY_REGISTER_UI("input")

- (void)layoutDidFinished {
  self.view.padding = self.padding;
  }

LYNX_PROP_SETTER("value", setValue, NSString \*) {
self.view.text = value;
}

- (UITextField *)createView {
  UITextField *textField = [[LynxTextField alloc] init];
  //...
  textField.delegate = self;
  return textField;
  }

@end

@implementation LynxTextField

- (UIEditingInteractionConfiguration)editingInteractionConfiguration API_AVAILABLE(ios(13.0)) {
  return UIEditingInteractionConfigurationNone;
  }

- (void)setPadding:(UIEdgeInsets)padding {
  \_padding = padding;
  [self setNeedsLayout];
  }

- (CGRect)textRectForBounds:(CGRect)bounds {
  CGFloat x = self.padding.left;
  CGFloat y = self.padding.top;
  CGFloat width = bounds.size.width - self.padding.left - self.padding.right;
  CGFloat height = bounds.size.height - self.padding.top - self.padding.bottom;

  return CGRectMake(x, y, width, height);
  }

- (CGRect)editingRectForBounds:(CGRect)bounds {
  return [self textRectForBounds:bounds];
  }
  @end

Handle Event Binding (Optional)

In some scenarios, the front-end may need to respond to events from custom elements. For example, when the user types in the input box, the front-end might

need to capture and process the input data.

Here is an example of how to send a text input event from the <input> element to the front-end and how the front-end listens for the event.

Client-Side Event Sending

The client listens to text input callbacks from the native view, and when the text changes, it uses [self.context.eventEmitter dispatchCustomEvent:eventInfo] to send the event to the front-end for handling.

LynxExplorerInput.m
#import "LynxExplorerInput.h"
#import <Lynx/LynxComponentRegistry.h>
#import <Lynx/LynxPropsProcessor.h>

@implementation LynxExplorerInput

LYNX_LAZY_REGISTER_UI("input")

- (UITextField *)createView {
  UITextField *textField = [[LynxTextField alloc] init];
  //...
  textField.delegate = self;
  [[NSNotificationCenter defaultCenter] addObserver:self
                                        selector:@selector(textFieldDidChange:)
                                        name:UITextFieldTextDidChangeNotification
                                        object:textField];
  return textField;
}

- (void)emitEvent:(NSString *)name detail:(NSDictionary *)detail {
  LynxCustomEvent *eventInfo = [[LynxDetailEvent alloc] initWithName:name
                                                          targetSign:[self sign]
                                                              detail:detail];
  [self.context.eventEmitter dispatchCustomEvent:eventInfo];
}

- (void)textFieldDidChange:(NSNotification *)notification {
  [self emitEvent:@"input"
           detail:@{
             @"value": [self.view text] ?: @"",
           }];
}

- (void)layoutDidFinished {
    self.view.padding = self.padding;
}

LYNX_PROP_SETTER("value", setValue, NSString *) {
    self.view.text = value;
}

@end

@implementation LynxTextField

- (UIEditingInteractionConfiguration)editingInteractionConfiguration API_AVAILABLE(ios(13.0)) {
  return UIEditingInteractionConfigurationNone;
}

- (void)setPadding:(UIEdgeInsets)padding {
  _padding = padding;
  [self setNeedsLayout];
}

- (CGRect)textRectForBounds:(CGRect)bounds {
  CGFloat x = self.padding.left;
  CGFloat y = self.padding.top;
  CGFloat width = bounds.size.width - self.padding.left - self.padding.right;
  CGFloat height = bounds.size.height - self.padding.top - self.padding.bottom;

  return CGRectMake(x, y, width, height);
}

- (CGRect)editingRectForBounds:(CGRect)bounds {
  return [self textRectForBounds:bounds];
}
@end

Front-End DSL Event Binding

On the front-end, bind the corresponding input event to listen for and handle the text input data sent by the client.

App.tsx
const handleInput = (e) => {
  const currentValue = e.detail.value.trim();
  setInputValue(currentValue);
};

<input className="input-box" bindinput={handleInput} value={inputValue} />;

Note: The front-end DSL uses bindxxx for event binding, such as bindinput to bind the input event.

Support Direct Element Manipulation (Optional)

In some cases, the front-end may need to directly manipulate custom elements via imperative APIs. You can make elements support such operations with LYNX_UI_METHOD.

Front-End Call Example

The following code shows how to use SelectorQuery to call the focus method and focus the <input> element:

App.tsx
lynx
  .createSelectorQuery()
  .select('#input-id')
  .invoke({
    method: 'focus',
    params: {},
    success: function (res) {
      console.log('lynx', 'request focus success');
    },
    fail: function (res) {
      console.log('lynx', 'request focus fail');
    },
  })
  .exec();

Client-Side Implementation

On the client side, use LYNX_UI_METHOD to add a focus method to the custom element to handle the front-end call.

LynxExplorerInput.m
#import "LynxExplorerInput.h"
#import <Lynx/LynxComponentRegistry.h>
#import <Lynx/LynxPropsProcessor.h>
#import <Lynx/LynxUIMethodProcessor.h>

@implementation LynxExplorerInput

LYNX_LAZY_REGISTER_UI("input")

LYNX_UI_METHOD(focus) {
    if ([self.view becomeFirstResponder]) {
        callback(kUIMethodSuccess, nil);
    } else {
        callback(kUIMethodUnknown, @"fail to focus");
    }
}

- (UITextField *)createView {
  UITextField *textField = [[LynxTextField alloc] init];
  //...
  textField.delegate = self;
  [[NSNotificationCenter defaultCenter] addObserver:self
                                        selector:@selector(textFieldDidChange:)
                                        name:UITextFieldTextDidChangeNotification
                                        object:textField];
  return textField;
}

- (void)emitEvent:(NSString *)name detail:(NSDictionary *)detail {
  LynxCustomEvent *eventInfo = [[LynxDetailEvent alloc] initWithName:name
                                                          targetSign:[self sign]
                                                              detail:detail];
  [self.context.eventEmitter dispatchCustomEvent:eventInfo];
}

- (void)textFieldDidChange:(NSNotification *)notification {
  [self emitEvent:@"input"
           detail:@{
             @"value": [self.view text] ?: @"",
           }];
}

- (void)layoutDidFinished {
    self.view.padding = self.padding;
}

LYNX_PROP_SETTER("value", setValue, NSString *) {
    self.view.text = value;
}

@end

@implementation LynxTextField

- (UIEditingInteractionConfiguration)editingInteractionConfiguration API_AVAILABLE(ios(13.0)) {
  return UIEditingInteractionConfigurationNone;
}

- (void)setPadding:(UIEdgeInsets)padding {
  _padding = padding;
  [self setNeedsLayout];
}

- (CGRect)textRectForBounds:(CGRect)bounds {
  CGFloat x = self.padding.left;
  CGFloat y = self.padding.top;
  CGFloat width = bounds.size.width - self.padding.left - self.padding.right;
  CGFloat height = bounds.size.height - self.padding.top - self.padding.bottom;

  return CGRectMake(x, y, width, height);
}

- (CGRect)editingRectForBounds:(CGRect)bounds {
  return [self textRectForBounds:bounds];
}
@end

Method Callback Return Values

When implementing the focus method, component developers need to return a status code to the frontend to indicate whether the operation was successful. For instance, the frontend call might fail, in which case an appropriate error status should be returned so that the frontend can handle it in the fail callback.

Lynx Engine defines several common error codes, and developers can return the appropriate status code in the method callback:

enum LynxUIMethodErrorCode {
  kUIMethodSuccess = 0, // Succeeded
  kUIMethodUnknown, // Unknown error
  kUIMethodNodeNotFound, // Cannot find corresponding element
  kUIMethodMethodNotFound, // No corresponding method on this element
  kUIMethodParamInvalid, // Invalid method parameters
  kUIMethodSelectorNotSupported, // Selector not supported
};

Custom Native element Implementation Process

The implementation of custom native elements involves several steps, including: declaring and registering the element, creating native views, handling styles and properties, event binding, etc. Let's take a simple custom input element <input> as an example to briefly introduce the implementation process of a custom element. The complete code can be viewed in LynxExplorer.

The complete implementation can be found in the LynxExplorer/input module. You can compile and run the LynxExplorer sample project to preview element behavior in real-time.

Declaring and Registering the Element

Declare the Custom Element

The declared custom element needs to inherit from LynxUI.

Java
Kotlin
LynxExplorerInput.java

import com.lynx.tasm.behavior.LynxContext;
import com.lynx.tasm.behavior.ui.LynxUI;
import androidx.appcompat.widget.AppCompatEditText;


public class LynxExplorerInput extends LynxUI<AppCompatEditText> {

  public LynxExplorerInput(LynxContext context) {
    super(context);
  }
  //...

}

Register the Custom Element

There are two ways to register elements: global registration and local registration.

Global Registration

Globally registered elements can be shared among multiple LynxView instances.

Java
Kotlin

import com.lynx.tasm.LynxEnv;
import com.lynx.tasm.behavior.Behavior;

LynxEnv.inst().addBehavior(new Behavior("input"){
      @Override
      public LynxExplorerInput createUI(LynxContext context) {
        return new LynxExplorerInput(context);
      }
    });
Local Registration

Locally registered elements are only available for the current LynxView instance.

Java
Kotlin

LynxViewBuilder lynxViewBuilder = new LynxViewBuilder();
lynxViewBuilder.addBehavior(new Behavior("input") {
      @Override
      public LynxExplorerInput createUI(LynxContext context) {
        return new LynxExplorerInput(context);
      }
    });

Where "input" corresponds to the tag name in the front-end DSL. When the Lynx Engine encounters this tag, it will look for the registered native element and create an instance.

Create the NativeView Instance

Each custom element needs to implement the createView method, which returns the corresponding native View instance.

Here’s the implementation for the <input> element:

Java
Kotlin
LynxExplorerInput.java

import android.content.Context;
import androidx.appcompat.widget.AppCompatEditText;
import com.lynx.tasm.behavior.LynxContext;
import com.lynx.tasm.behavior.ui.LynxUI;

public class LynxExplorerInput extends LynxUI<AppCompatEditText> {

  public LynxExplorerInput(LynxContext context) {
    super(context);
  }

  @Override
  protected AppCompatEditText createView(Context context) {
    AppCompatEditText view = new AppCompatEditText(context);
    //...
    return view;
  }

}

Handle Front-End Style and Property Updates

You can use the @LynxProp annotation to listen for property changes passed from the front-end and update the native view accordingly. For example, handling the value property of the <input> element:

Java
Kotlin
LynxExplorerInput.java

import android.content.Context;
import androidx.appcompat.widget.AppCompatEditText;
import com.lynx.tasm.behavior.LynxContext;
import com.lynx.tasm.behavior.LynxProp;
import com.lynx.tasm.behavior.ui.LynxUI;

public class LynxExplorerInput extends LynxUI<AppCompatEditText> {

  public LynxExplorerInput(LynxContext context) {
    super(context);
  }

  @LynxProp(name = "value")
  public void setValue(String value) {
    if (!value.equals(mView.getText().toString())) {
      mView.setText(value);
    }
  }

  @Override
  protected AppCompatEditText createView(Context context) {
    AppCompatEditText view = new AppCompatEditText(context);
    //...
    return view;
  }



}

Handle Layout Information (Optional)

Handle the Layout Result from the Lynx Engine

Usually, the Lynx Engine will automatically calculate and update the View layout information, so developers don’t need to handle this manually. However, in some special cases, such as when extra adjustments are needed for the View, you can retrieve the latest layout information in the onLayoutUpdated callback and apply custom logic.

Java
Kotlin
LynxExplorerInput.java

import android.content.Context;
import androidx.appcompat.widget.AppCompatEditText;
import com.lynx.tasm.behavior.LynxContext;
import com.lynx.tasm.behavior.LynxProp;
import com.lynx.tasm.behavior.ui.LynxUI;

public class LynxExplorerInput extends LynxUI<AppCompatEditText> {

  public LynxExplorerInput(LynxContext context) {
    super(context);
  }

  @Override
  public void onLayoutUpdated() {
    super.onLayoutUpdated();
    int paddingTop = mPaddingTop + mBorderTopWidth;
    int paddingBottom = mPaddingBottom + mBorderBottomWidth;
    int paddingLeft = mPaddingLeft + mBorderLeftWidth;
    int paddingRight = mPaddingRight + mBorderRightWidth;
    mView.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
  }

  @Override
  protected AppCompatEditText createView(Context context) {
    AppCompatEditText view = new AppCompatEditText(context);
    //...
    return view;
  }

  @LynxProp(name = "value")
  public void setValue(String value) {
    if (!value.equals(mView.getText().toString())) {
      mView.setText(value);
    }
  }

}

Event Binding

Event handling in native elements is usually done using the @LynxEvent annotation, which binds events between the front-end and native elements. For example, let’s implement a custom onChange event for the <input> element:

Java
Kotlin
LynxExplorerInput.java
import android.content.Context;
import android.text.Editable;
import android.text.TextWatcher;
import androidx.appcompat.widget.AppCompatEditText;
import com.lynx.tasm.behavior.LynxContext;
import com.lynx.tasm.behavior.LynxProp;
import com.lynx.tasm.behavior.ui.LynxUI;
import com.lynx.tasm.event.LynxCustomEvent;
import java.util.HashMap;
import java.util.Map;

public class LynxExplorerInput extends LynxUI<AppCompatEditText> {

  private void emitEvent(String name, Map<String, Object> value) {
    LynxCustomEvent detail = new LynxCustomEvent(getSign(), name);
    if (value != null) {
      for (Map.Entry<String, Object> entry : value.entrySet()) {
        detail.addDetail(entry.getKey(), entry.getValue());
      }
    }
    getLynxContext().getEventEmitter().sendCustomEvent(detail);
  }

  @Override
  protected AppCompatEditText createView(Context context) {
    AppCompatEditText view = new AppCompatEditText(context);
    view.addTextChangedListener(new TextWatcher() {
      @Override
      public void afterTextChanged(Editable s) {
        emitEvent("input", new HashMap<String, Object>() {
          {
            put("value", s.toString());
          }
        });
      }

      @Override
      public void beforeTextChanged(CharSequence s, int start, int count, int after) {}

      @Override
      public void onTextChanged(CharSequence s, int start, int before, int count) {
      }
    });
    return view;

  }

  public LynxExplorerInput(LynxContext context) {
    super(context);
  }

  @Override
  public void onLayoutUpdated() {
    super.onLayoutUpdated();
    int paddingTop = mPaddingTop + mBorderTopWidth;
    int paddingBottom = mPaddingBottom + mBorderBottomWidth;
    int paddingLeft = mPaddingLeft + mBorderLeftWidth;
    int paddingRight = mPaddingRight + mBorderRightWidth;
    mView.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
  }

  @LynxProp(name = "value")
  public void setValue(String value) {
    if (!value.equals(mView.getText().toString())) {
      mView.setText(value);
    }
  }

}

Front-End DSL Event Binding

On the front-end, you need to bind the relevant input events for the text box. With the following code, the front-end will listen for events sent by the client and process the input data as needed.

App.tsx
const handleInput = (e) => {
  const currentValue = e.detail.value.trim();
  setInputValue(currentValue);
};

<input className="input-box" bindinput={handleInput} value={inputValue} />;

Note: Front-end DSL uses bindxxx for event binding, such as bindinput for binding the input event.

Supporting Direct Element Manipulation (Optional)

In some cases, the front-end may need to directly manipulate custom elements using imperative APIs. You can enable such operations on elements by using @LynxUIMethod.

Front-End Example Call

The following code demonstrates how to use the SelectorQuery API to call the focus method and make the <input> element gain focus:

App.tsx
lynx
  .createSelectorQuery()
  .select('#input-id')
  .invoke({
    method: 'focus',
    params: {},
    success: function (res) {
      console.log('lynx', 'request focus success');
    },
    fail: function (res) {
      console.log('lynx', 'request focus fail');
    },
  })
  .exec();

Client-Side Implementation

On the client side, you need to add the focus method to your custom element using @LynxUIMethod, ensuring it can correctly handle the front-end call.

Java
Kotlin
LynxExplorerInput.java
import android.content.Context;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.inputmethod.InputMethodManager;
import androidx.appcompat.widget.AppCompatEditText;
import com.lynx.react.bridge.Callback;
import com.lynx.react.bridge.ReadableMap;
import com.lynx.tasm.behavior.LynxContext;
import com.lynx.tasm.behavior.LynxProp;
import com.lynx.tasm.behavior.LynxUIMethod;
import com.lynx.tasm.behavior.LynxUIMethodConstants;
import com.lynx.tasm.behavior.ui.LynxUI;
import com.lynx.tasm.event.LynxCustomEvent;

import java.util.HashMap;
import java.util.Map;

public class LynxExplorerInput extends LynxUI<AppCompatEditText> {

  private boolean showSoftInput() {
    InputMethodManager imm = (InputMethodManager) getLynxContext().getSystemService(Context.INPUT_METHOD_SERVICE);
    return imm.showSoftInput(mView,
      InputMethodManager.SHOW_IMPLICIT, null);
  }

  @LynxUIMethod
  public void focus(ReadableMap params, Callback callback) {
    if (mView.requestFocus()) {
      if (showSoftInput()) {
        callback.invoke(LynxUIMethodConstants.SUCCESS);
      } else {
        callback.invoke(LynxUIMethodConstants.UNKNOWN, "fail to show keyboard");
      }
    } else {
      callback.invoke(LynxUIMethodConstants.UNKNOWN, "fail to focus");
    }
  }

  private void emitEvent(String name, Map<String, Object> value) {
    LynxCustomEvent detail = new LynxCustomEvent(getSign(), name);
    if (value != null) {
      for (Map.Entry<String, Object> entry : value.entrySet()) {
        detail.addDetail(entry.getKey(), entry.getValue());
      }
    }
    getLynxContext().getEventEmitter().sendCustomEvent(detail);
  }

  @Override
  protected AppCompatEditText createView(Context context) {
    AppCompatEditText view = new AppCompatEditText(context);
    view.addTextChangedListener(new TextWatcher() {
      @Override
      public void afterTextChanged(Editable s) {
        emitEvent("input", new HashMap<String, Object>() {
          {
            put("value", s.toString());
          }
        });
      }

      @Override
      public void beforeTextChanged(CharSequence s, int start, int count, int after) {
      }

      @Override
      public void onTextChanged(CharSequence s, int start, int before, int count) {
      }
    });
    return view;
  }

  public LynxExplorerInput(LynxContext context) {
    super(context);
  }

  @Override
  public void onLayoutUpdated() {
    super.onLayoutUpdated();
    int paddingTop = mPaddingTop + mBorderTopWidth;
    int paddingBottom = mPaddingBottom + mBorderBottomWidth;
    int paddingLeft = mPaddingLeft + mBorderLeftWidth;
    int paddingRight = mPaddingRight + mBorderRightWidth;
    mView.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
  }

  @LynxProp(name = "value")
  public void setValue(String value) {
    if (!value.equals(mView.getText().toString())) {
      mView.setText(value);
    }
  }
}

Method Callback Return Values

When implementing the focus method, component developers need to return a status code to the frontend to indicate whether the operation was successful. For instance, the frontend call might fail, in which case an appropriate error status should be returned so that the frontend can handle it in the fail callback.

Lynx Engine predefines some common error codes, and the element developer can return the appropriate status code in the method callback:

enum LynxUIMethodErrorCode {
  kUIMethodSuccess, // Succeeded
  kUIMethodUnknown, // Unknown error
  kUIMethodNodeNotFound, // Cannot find corresponding element
  kUIMethodMethodNotFound, // No corresponding method on this element
  kUIMethodParamInvalid, // Invalid method parameters
  kUIMethodSelectorNotSupported, // Selector not supported
}

The way to customize elements in the web can directly refer to Web Components

Use your Native Element

Once you have completed the development of a custom element, you can use it just like a built-in element. Below is a simple example of using an <input> element:

Except as otherwise noted, this work is licensed under a Creative Commons Attribution 4.0 International License, and code samples are licensed under the Apache License 2.0.