环境安装
- node 环境,建议安装v18以上版本
- nodejs官网下载安装包,或通过nvm安装
node -v
- 全局安装yo
npm install -g yo
- 全局安装 @mendix/generator-widget
npm install -g @mendix/generator-widget
使用
- 初始化项目
yo @mendix/widget myWidget
- 进入组件目录并打包,将组件更新到mendix本地工程中
cd myWidget && npm run build
组件目录说明
go
- dist
- node_modules
- src
components
HelloWorldSample.tsx // 默认生成的子组件
ui
xxx.css // 可修改为 scss文件,
package.xml
xxx.editorConfig.ts // xml配置信息调整,比如某个xml字段的显示隐藏等操作
xxx.editorPreview.tsx // mendix预览模式下的展示内容
xxx.tsx // 组件渲染的内容文件
xxx.xml // 配置组件需要用到的参数信息
- typings
xxx.d.ts // xml文件配置的字段类型,自动生成的
xml配置项说明
xml
<?xml version="1.0" encoding="utf-8" ?>
<widget
id="mendix.xxx.xxx"
pluginWidget="true"
needsEntityContext="true"
offlineCapable="true"
supportedPlatform="Web"
xmlns="http://www.mendix.com/widget/1.0/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.mendix.com/widget/1.0/ ../node_modules/mendix/custom_widget.xsd"
>
<name>xxx</name>
<description>My widget description</description>
<icon />
<properties>
<propertyGroup caption="General">
<!-- Data source分组 -->
<propertyGroup caption="Data source">
<property key="myDatasource" type="datasource" isList="true">
<caption>Data Source</caption>
<description>关联数据源</description>
</property>
<property key="mode" type="enumeration" defaultValue="undefined">
<caption>Mode</caption>
<description>枚举类型</description>
<enumerationValues>
<enumerationValue key="undefined">Single</enumerationValue>
<enumerationValue key="multiple">Multiple</enumerationValue>
<enumerationValue key="tags">Tags</enumerationValue>
</enumerationValues>
</property>
<property key="showCode" type="boolean" defaultValue="false">
<caption>Show Prefix</caption>
<description>布尔值</description>
</property>
<property key="code" type="attribute" dataSource="myDatasource">
<caption>Option Prefix</caption>
<description>从myDatasource中选取某个字段,需要指定类型</description>
<attributeTypes>
<!-- 根据字段类型显示字段 -->
<attributeType name="String" />
<attributeType name="AutoNumber" />
<attributeType name="Boolean" />
<attributeType name="DateTime" />
<attributeType name="Decimal" />
<attributeType name="Enum" />
<attributeType name="Integer" />
<attributeType name="Long" />
</attributeTypes>
</property>
<property key="labelName" type="attribute" dataSource="myDatasource">
<caption>Label Name</caption>
<description>从myDatasource中选取某个字段,需要指定类型</description>
<attributeTypes>
<attributeType name="String" />
</attributeTypes>
</property>
<property key="placeholder" type="string" defaultValue="placeholder">
<caption>PlaceHolder</caption>
<description>mendix配置的字符串</description>
</property>
<property key="isRequired" type="expression" required="true" defaultValue="false">
<caption>Is Required</caption>
<description>条件表达式,定义返回数据类型</description>
<returnType type="Boolean" />
</property>
</propertyGroup>
<!-- Events分组 -->
<propertyGroup caption="Events">
<property key="onChangeAction" type="action" required="false">
<caption>Value Change</caption>
<description>事件</description>
</property>
</propertyGroup>
</propertyGroup>
</properties>
</widget>
更多文档内容,可参考Mendix组件文档
xml配置项的显示/隐藏方式
-
修改
xxx.editorConfig.ts
文件typescriptimport { xxxProps } from "../typings/xxx"; import { hidePropertyIn } from "@mendix/pluggable-widgets-tools"; // 新增依赖 // ... // ... // ... export function getProperties(_values: xxxProps, defaultProperties: Properties): Properties { // 主要代码 if (!_values.showCode) { hidePropertyIn(defaultProperties, _values, "code"); } if (!_values.isRequired) { hidePropertyIn(defaultProperties, _values, "errorText"); } return defaultProperties; }
组件参数说明
-
组件默认接收一个对象,默认参数在mendix中可配置且,包含有:
- name
- class
- style:非必传
- tabIndex:非必传
-
用法
typescriptimport { ReactElement, createElement } from "react"; import classNames from "classnames"; import { xxxProps } from "../typings/xxxProps"; import "./ui/xxx.scss"; export function xxx(props: xxxProps): ReactElement { const { name, class: cls, style, tabIndex } = props; return ( <div className={classNames(cls, "v-test-wrap")} style={style}> <p>Container</p> </div> ); }
如需要用到mendix挂载在window上的mx方法,可在 typings 目录下添加 client.d.ts
typescript
declare namespace mx {
namespace ui {
type OpenForm2Function = ((
page: string,
dh: DisposeCallback,
title: string,
currentForm: any,
option: Option,
numberOfPagesToClose: number
) => Promise<any>) & { ["_tabRouter"]: boolean };
interface DisposeCallback {
[key: string]: { unsubscribe: () => void };
}
interface Option {
location: "content" | "popup" | "node";
domNode?: Element;
}
let openForm2: OpenForm2Function;
let getContentForm: any;
}
namespace data {
type saveDocument = (
guid: string,
name: string,
params: any,
blob: File,
callback: () => void,
errorCallback: (error: any) => void
) => Promise<void>;
type remove = (params: RemoveType) => Promise<void>;
interface RemoveType {
guid: string;
callback: () => void;
errorCallback: (error: any) => void;
}
type action = (
actionname: any,
applyto: any,
guids: any[],
params: any,
callback: (result: any) => void,
errorCallback: (error: any) => void
) => Promise<void>;
type callNanoflow = (
nanoflow: any,
context: mx.lib.MxContext,
origin: mx.lib.form._FormBase,
callback: (result: any) => void,
errorCallback: (error: any) => void
) => Promise<void>;
type create = (
entity: string,
callback: (guid: string) => void,
errorCallback: (error: any) => void
) => Promise<void>;
const action = action;
const callNanoflow = callNanoflow;
const saveDocument = saveDocument;
const remove = remove;
const create = create;
}
}
declare namespace mendix {
namespace lib {
class MxContext {
setTrackObject(obj: any): void;
}
}
interface Lang {
getUniqueId(): string;
}
let lang: Lang;
}
declare namespace dijit {
function getUniqueId(id: string): string;
}
declare namespace window {
namespace vRequestManager {
function request(url: string, options: any, requestId: string): Promise<any>;
function vCreateObjFn(obj: any): Promise<any>;
function cancelRequest(id: string): Promise<void>;
}
}
mendix工程,默认是不会生成 index.html
文件的,需要手动生成,方式如下
-
mendix 菜单栏 App -> Show App Directory in Explorer
-
xxx/theme/web
目录下,就是项目启动的入口文件所在位置 -
index.html
文件内容html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> <meta http-equiv="Pragma" content="no-cache" /> <meta http-equiv="Expires" content="0" /> <meta name="referrer" content="no-referrer" /> <title>Mendix</title> <script type="text/javascript"> // 定义全局 hash globalThis.hash = Math.floor(Math.random() * 0xffffff).toString(16); try { eval("async () => {}"); } catch (error) { var homeUrl = window.location.origin + window.location.pathname; var appUrl = homeUrl.slice(0, homeUrl.lastIndexOf("/") + 1); window.location.replace(appUrl + "unsupported-browser.html"); } </script> </head> <body> <noscript>To use this application, please enable JavaScript.</noscript> <div id="content"></div> </body> <script type="text/javascript"> dojoConfig = { isDebug: false, useCustomLogger: true, async: true, baseUrl: "mxclientsystem/dojo/", cacheBust: globalThis.hash, rtlRedirect: "index-rtl.html", }; // 需要添加script标签的地方 const jsSrcList = [ "mxclientsystem/mxui/mxui.js" ]; for (const src of jsSrcList) { const scriptEl = document.createElement("script"); scriptEl.src = `${src}?s=${globalThis.hash}`; document.body.appendChild(scriptEl); } // 加载 link 标签 const iconList = [ // manifest { rel: "manifest", href: "manifest.webmanifest", crossorigin: "use-credentials", }, // main.scss 编译之后文件 { rel: "stylesheet", href: "theme.compiled.css" }, // icon { rel: "apple-touch-icon", href: "apple-touch-icon.png", sizes: "180x180", }, { rel: "icon", href: "icon-32.png", sizes: "32x32" }, { rel: "icon", href: "icon-16.png", sizes: "16x16" }, // apple-touch-startup-image { custorm: true, width: 1024, height: 1366, devicePixelRatio: 2, orientation: "portrait", }, { custorm: true, width: 1366, height: 1024, devicePixelRatio: 2, orientation: "landscape", }, { custorm: true, width: 834, height: 1194, devicePixelRatio: 2, orientation: "portrait", }, { custorm: true, width: 1194, height: 834, devicePixelRatio: 2, orientation: "landscape", }, { custorm: true, width: 834, height: 1112, devicePixelRatio: 2, orientation: "portrait", }, { custorm: true, width: 1112, height: 834, devicePixelRatio: 2, orientation: "landscape", }, { custorm: true, width: 810, height: 1080, devicePixelRatio: 2, orientation: "portrait", }, { custorm: true, width: 1080, height: 810, devicePixelRatio: 2, orientation: "landscape", }, { custorm: true, width: 768, height: 1024, devicePixelRatio: 2, orientation: "portrait", }, { custorm: true, width: 1024, height: 768, devicePixelRatio: 2, orientation: "landscape", }, { custorm: true, width: 428, height: 926, devicePixelRatio: 3, orientation: "portrait", }, { custorm: true, width: 926, height: 428, devicePixelRatio: 3, orientation: "landscape", }, { custorm: true, width: 390, height: 844, devicePixelRatio: 3, orientation: "portrait", }, { custorm: true, width: 844, height: 390, devicePixelRatio: 3, orientation: "landscape", }, { custorm: true, width: 360, height: 780, devicePixelRatio: 3, orientation: "portrait", }, { custorm: true, width: 780, height: 360, devicePixelRatio: 3, orientation: "landscape", }, { custorm: true, width: 414, height: 896, devicePixelRatio: 3, orientation: "portrait", }, { custorm: true, width: 896, height: 414, devicePixelRatio: 3, orientation: "landscape", }, { custorm: true, width: 414, height: 896, devicePixelRatio: 2, orientation: "portrait", }, { custorm: true, width: 896, height: 414, devicePixelRatio: 2, orientation: "landscape", }, { custorm: true, width: 375, height: 812, devicePixelRatio: 3, orientation: "portrait", }, { custorm: true, width: 812, height: 375, devicePixelRatio: 3, orientation: "landscape", }, { custorm: true, width: 375, height: 667, devicePixelRatio: 2, orientation: "portrait", }, { custorm: true, width: 667, height: 375, devicePixelRatio: 2, orientation: "landscape", }, { custorm: true, width: 414, height: 736, devicePixelRatio: 3, orientation: "portrait", }, { custorm: true, width: 736, height: 414, devicePixelRatio: 3, orientation: "landscape", }, { custorm: true, width: 320, height: 568, devicePixelRatio: 2, orientation: "portrait", }, { custorm: true, width: 568, height: 320, devicePixelRatio: 2, orientation: "landscape", }, ]; for (const items of iconList) { const link = document.createElement("link"); if (items.custorm) { link.rel = "apple-touch-startup-image"; link.href = `img/startup-image-${ items.width * items.devicePixelRatio }x${items.height * items.devicePixelRatio}.png?${globalThis.hash}`; link.media = `screen and (device-width: ${items.width}px) and (device-height: ${items.height}px) and (-webkit-device-pixel-ratio: ${items.devicePixelRatio}) and (orientation: ${items.orientation})`; } else { link.rel = items.rel; if (items.sizes) { link.sizes = items.sizes; } if (items.crossorigin) { link.crossorigin = items.crossorigin; } link.href = `${items.href}?l=${globalThis.hash}`; } document.head.appendChild(link); } // 处理登录页面的 cookie if (!document.cookie || !document.cookie.match(/(^|;) *originURI=/gi)) { const url = new URL(window.location.href); const subPath = url.pathname.substring(0, url.pathname.lastIndexOf("/")); document.cookie = `originURI=${subPath}/login.html${ window.location.protocol === "https:" ? ";SameSite=None;Secure" : "" }`; } </script> </html>