Skip to content

Form Cell Extension

The extension class for a Form Cell Extension must extend the IControl or IView interface.

If the life-cycle calls through is not needed for the extension class, it may inherit from the IView interface.

For Mobile Development Kit version 6.2 or older, the return value of the view() method must be a subclass of a native view (i.e. a subclass of UIView for iOS or android.view.View for Android) for section/table and form cell extensions. The viewIsNative() must also be overridden to return true.

With Mobile Development Kit version 6.3 or newer, it is possible to return a NativeScript view through the view() method. In this case, the viewIsNative() must also be overridden to return false.

Example - Creating Extension Using Native Views via TypeScript Marshalling

Implementing the Extension control to return native iOS / Android views using TypeScript Marshalling.

You have the following the file in the Extensions folder: /MyMDKExtensionModule/controls/MyFormCellExtensionControl.ts:

//file: MyFormCellExtensionControl.ts
import * as app from '@nativescript/core/application';
import { BaseObservable } from 'mdk-core/observables/BaseObservable';
import { Color } from '@nativescript/core/color';
import { IControl } from 'mdk-core/controls/IControl';

export class MyFormCellExtension extends IControl {
  private _control: any;
  private _observable: BaseObservable;
  private _value: any;

  public initialize(props) {
    super.initialize(props);
    if (this.definition().data.ExtensionProperties.Prop1) {
      this._value = this.definition().data.ExtensionProperties.Prop1.Value;
    }
    const color = new Color('lightgray');
    if (app.ios) {
      this._control = UITextField.alloc().init();
      this._control.backgroundColor = color.ios;
    } else {
      this._control = new android.widget.EditText(this.androidContext());
      this._control.setBackgroundColor(color.android);
    }
  }

  public view() {
    //Use provided ValueResolver to resolve value to support bindings, rules, etc in your extension
    this.valueResolver().resolveValue(this._value).then((resolvedValue)=> {
      if (app.ios) {
        this._control.text = resolvedValue;
      } else {
        this._control.setText(resolvedValue);
      }
    });
    return this._control;
  }

  public viewIsNative() {
    return true;
  }

  public observable() {
    if (!this._observable) {
      this._observable = new BaseObservable(this, this.definition(), this.page());
    }
    return this._observable;
  }

  public setValue(value: any, notify: boolean): Promise<any> {
    this._value = value;
    if (app.ios) {
      this._control.text = value;
    } else {
      this._control.setText(value);
    }
    return Promise.resolve();
  }

  public setContainer(container: IControl) {
    // do nothing
  }
}

Consuming the extension control in your app e.g. in a Form Cell Section

{
  "Module": "MyMDKExtensionModule",
  "Control": "MyFormCellExtensionControl",
  "Class": "MyFormCellExtension",
  "ExtensionProperties": {
    "Prop1": {
      "Value": "Value for this extension"
    },
    "Event1": "/MDKApp/Actions/Messages/ShowMessage.action",
    ...
  },
  "_Type": "Control.Type.FormCell.Extension"
}

Example - Creating Extension Using NativeScript Views

Implementing the Extension control to return NativeScript views.

You have the following the file in the Extensions folder: /MyMDKExtensionModule/controls/NSSliderControl.ts:

//file: NSSliderControl.ts
import { GridLayout, Label, PropertyChangeData, Slider } from '@nativescript/core';
import { IControl } from 'mdk-core/controls/IControl';
import { BaseObservable } from 'mdk-core/observables/BaseObservable';

function toNumber(value: any, defaultIfNaN = 0): number {
  if ((typeof value !== 'number' && typeof value !== 'string') || (typeof value === 'string' && value.trim().length === 0)) {
    return defaultIfNaN;
  }
  const n = +value;
  return isNaN(n) ? defaultIfNaN : n;
}

function toNumberGenerator(defaultIfNaN = 0): (value: any) => number {
  return (value: any) => toNumber(value, defaultIfNaN);
}

export class NSSlider extends IControl {
  private _nsView: GridLayout;
  private _slider: Slider;
  private _sliderValueText: Label;
  private _unit = '';
  private _observable: BaseObservable;

  public view(): any {
    const defaultMinValue = 0;
    const defaultMaxValue = 10000;

    if (!this._nsView) {
      this._nsView = new GridLayout();
      (this._nsView as any).rows = 'auto,auto';
      (this._nsView as any).columns = 'auto,*,auto';
      const headerGrid = new GridLayout();
      const sliderLabel = new Label();
      sliderLabel.className = 'slider-label';
      const sliderValue = new Label();
      this._sliderValueText = sliderValue;
      sliderValue.className = 'slider-value';
      headerGrid.colSpan = 3;
      headerGrid.addChild(sliderLabel);
      headerGrid.addChild(sliderValue);
      headerGrid.className = 'header-wrapper';

      const slider = new Slider();
      this._slider = slider;
      slider.row = 1;
      slider.col = 1;
      slider.className = 'slider';
      slider.on('valueChange', (evt: PropertyChangeData) => {
        this.setValue(evt.value, true, false);
      });
      const sliderMinValueLabel = new Label();
      sliderMinValueLabel.row = 1;
      sliderMinValueLabel.col = 0;
      sliderMinValueLabel.className = 'slider-min-value-label';
      const sliderMaxValueLabel = new Label();
      sliderMaxValueLabel.row = 1;
      sliderMaxValueLabel.col = 2;
      sliderMaxValueLabel.className = 'slider-max-value-label';

      this._nsView.className = 'wrapper';
      this._nsView.addChild(headerGrid);
      this._nsView.addChild(slider);
      this._nsView.addChild(sliderMinValueLabel);
      this._nsView.addChild(sliderMaxValueLabel);
      this._nsView.addCss(`
      .wrapper {
        padding: 20;
      }
      .slider-label {
        horizontal-align: left;
        font-weight: bold;
      }
      .slider-value {
        horizontal-align: right;
      }

      .header-wrapper {
        margin-bottom: 10;
      }
      .slider {
        margin-left: 10;
        margin-right: 10;
        vertical-align: middle;
      }
      .slider-min-value-label, .slider-max-value-label {
        vertical-align: middle;
      }

      `);

      // set the unit
      this.resolve(this.definition().data.ExtensionProperties.Unit).then((unit) => {
        this._unit = unit ?? '';
        // update the text
        this._setSliderValueText(this._slider.value);
      });

      // set title
      this.resolve(this.definition().data.ExtensionProperties.Title).then((v) => {
        sliderLabel.text = `${v ?? ''}`;
      });

      // set min value
      this.resolve(this.definition().data.ExtensionProperties.MinValue)
        .then(toNumberGenerator(defaultMinValue))
        .then((v) => {
          slider.minValue = v;
          sliderMinValueLabel.text = `${v}`;
        });

      // set max value
      this.resolve(this.definition().data.ExtensionProperties.MaxValue)
        .then(toNumberGenerator(defaultMaxValue))
        .then((v) => {
          slider.maxValue = v;
          sliderMaxValueLabel.text = `${v}`;
        });

      // set start value
      this.resolve(this.definition().data.Value).then((v) => {
        this.setValue(v, false, false);
      });
    }
    return this._nsView;
  }

  public viewIsNative() {
    return false;
  }

  public observable() {
    if (!this._observable) {
      const onValueChange = this.definition().data.ExtensionProperties?.OnValueChange;
      if (onValueChange) {
        this.definition().data.OnValueChange = onValueChange;
      }
      this._observable = new BaseObservable(this, this.definition(), this.page());
    }
    return this._observable;
  }

  public setContainer(container: IControl) {
    // do nothing
  }

  public async setValue(value: any, notify: boolean, isTextValue?: boolean): Promise<any> {
    let finalNumber = toNumber(value, NaN);
    if (isNaN(finalNumber)) {
      throw new Error('Error: Value is not a number');
    }
    finalNumber = Math.max(this._slider.minValue, Math.min(this._slider.maxValue, finalNumber));
    this._setSliderValue(finalNumber);
    this._setSliderValueText(finalNumber);
    return this.observable().setValue(finalNumber, notify, isTextValue);
  }

  private resolve(v: any): Promise<any> {
    return this.valueResolver().resolveValue(v, this.context);
  }

  private _setSliderValue(value: number) {
    this._slider.value = value;
  }
  private _setSliderValueText(value: number) {
    const roundValue = Math.round(value);
    this._sliderValueText.text = `${roundValue}${this._unit ? ' ' + this._unit : ''}`;
  }
}

Consuming the extension control in your app e.g. in a Form Cell Section

{
  "Module": "MyMDKExtension",
  "Control": "NSSliderControl",
  "Class": "NSSlider",
  "Height": 200,
  "ExtensionProperties": {
      "MinValue": 0,
      "MaxValue": 100,
      "Unit": "mi",
      "Title": "Distance"
  },
  "Value": 10,
  "OnPress": "/MDKApp/Actions/ShowMessage.action",
  "OnValueChange": "/MDKApp/Rules/NSSliderOnValueChange.js",
  "_Type": "Control.Type.FormCell.Extension",
  "_Name": "MyNSSliderExtension1"
}

Last update: September 15, 2022