Current section

Files

Jump to
glendix README.md
Raw

README.md

# glendix
Gleam FFI bindings for React and Mendix Pluggable Widget API.
**JSX 없이, 순수 Gleam으둜 Mendix Pluggable Widget을 μž‘μ„±ν•œλ‹€.**
## Installation
```toml
# gleam.toml
[dependencies]
glendix = { path = "../glendix" }
```
> Hex νŒ¨ν‚€μ§€ 배포 μ „κΉŒμ§€λŠ” 둜컬 경둜둜 μ°Έμ‘°ν•©λ‹ˆλ‹€.
### Peer Dependencies
μœ„μ ― ν”„λ‘œμ νŠΈμ˜ `package.json`에 λ‹€μŒμ΄ ν•„μš”ν•©λ‹ˆλ‹€:
```json
{
"dependencies": {
"react": "^19.0.0",
"big.js": "^6.0.0"
}
}
```
## Quick Start
```gleam
import glendix/mendix
import glendix/react.{type JsProps, type ReactElement}
import glendix/react/html
import glendix/react/prop
pub fn widget(props: JsProps) -> ReactElement {
let name = mendix.get_string_prop(props, "sampleText")
html.div(prop.new() |> prop.class("my-widget"), [
react.text("Hello " <> name),
])
}
```
`fn(JsProps) -> ReactElement` β€” 이것이 Mendix Pluggable Widget의 μ „λΆ€μž…λ‹ˆλ‹€.
## Modules
### React
| Module | Description |
|---|---|
| `glendix/react` | 핡심 νƒ€μž… (`ReactElement`, `JsProps`, `Props`) + `el`, `fragment`, `text`, `none`, `when`, `when_some` |
| `glendix/react/prop` | Props νŒŒμ΄ν”„λΌμΈ λΉŒλ” β€” `prop.new() \|> prop.class("x") \|> prop.on_click(handler)` |
| `glendix/react/hook` | React Hooks β€” `use_state`, `use_effect`, `use_memo`, `use_callback`, `use_ref` |
| `glendix/react/event` | 이벀트 νƒ€μž… + `target_value`, `prevent_default`, `key` |
| `glendix/react/html` | HTML νƒœκ·Έ 편의 ν•¨μˆ˜ β€” `div`, `span`, `input`, `button` λ“± (순수 Gleam, FFI μ—†μŒ) |
| `glendix/binding` | μ™ΈλΆ€ React μ»΄ν¬λ„ŒνŠΈ 바인딩 β€” `.mjs` 없이 `bindings.json`만으둜 μ‚¬μš© |
### Mendix
| Module | Description |
|---|---|
| `glendix/mendix` | 핡심 νƒ€μž… (`ValueStatus`, `ObjectItem`) + JsProps μ ‘κ·Όμž (`get_prop`, `get_string_prop`) |
| `glendix/mendix/editable_value` | νŽΈμ§‘ κ°€λŠ₯ν•œ κ°’ β€” `value`, `set_value`, `set_text_value`, `display_value` |
| `glendix/mendix/action` | μ•‘μ…˜ μ‹€ν–‰ β€” `can_execute`, `execute`, `execute_if_can` |
| `glendix/mendix/dynamic_value` | 동적 읽기 μ „μš© κ°’ (ν‘œν˜„μ‹ 속성) |
| `glendix/mendix/list_value` | 리슀트 데이터 β€” `items`, `set_filter`, `set_sort_order`, `reload` |
| `glendix/mendix/list_attribute` | 리슀트 μ•„μ΄ν…œλ³„ μ ‘κ·Ό β€” `ListAttributeValue`, `ListActionValue`, `ListWidgetValue` |
| `glendix/mendix/selection` | 단일/닀쀑 선택 |
| `glendix/mendix/reference` | μ—°κ΄€ 관계 (단일 μ°Έμ‘°, 닀쀑 μ°Έμ‘°) |
| `glendix/mendix/date` | JS Date opaque 래퍼 (μ›”: Gleam 1-based ↔ JS 0-based μžλ™ λ³€ν™˜) |
| `glendix/mendix/big` | Big.js κ³ μ •λ°€ μ‹­μ§„μˆ˜ 래퍼 (`compare` β†’ `gleam/order.Order`) |
| `glendix/mendix/file` | `FileValue`, `WebImage` |
| `glendix/mendix/icon` | `WebIcon` β€” Glyph, Image, IconFont |
| `glendix/mendix/formatter` | `ValueFormatter` β€” `format`, `parse` |
| `glendix/mendix/filter` | FilterCondition λΉŒλ” β€” `and_`, `or_`, `equals`, `contains`, `attribute`, `literal` |
## Examples
### Props νŒŒμ΄ν”„λΌμΈ
```gleam
import glendix/react/prop
let props =
prop.new()
|> prop.class("btn btn-primary")
|> prop.string("type", "submit")
|> prop.bool("disabled", False)
|> prop.on_click(fn(_event) { Nil })
```
### useState + useEffect
```gleam
import glendix/react
import glendix/react/hook
import glendix/react/html
import glendix/react/prop
pub fn counter(_props) -> react.ReactElement {
let #(count, set_count) = hook.use_state(0)
hook.use_effect_once(fn() {
// 마운트 μ‹œ ν•œ 번 μ‹€ν–‰
Nil
})
html.div_([
html.button(
prop.new() |> prop.on_click(fn(_) { set_count(count + 1) }),
[react.text("Count: " <> int.to_string(count))],
),
])
}
```
### Mendix EditableValue 읽기/μ“°κΈ°
```gleam
import gleam/option.{None, Some}
import glendix/mendix
import glendix/mendix/editable_value as ev
pub fn render_input(props: react.JsProps) -> react.ReactElement {
case mendix.get_prop(props, "myAttribute") {
Some(attr) -> {
let display = ev.display_value(attr)
let editable = ev.is_editable(attr)
// ...
}
None -> react.none()
}
}
```
### 쑰건뢀 λ Œλ”λ§
```gleam
import glendix/react
import glendix/react/html
// Bool 기반
react.when(is_visible, fn() {
html.div_([react.text("Visible!")])
})
// Option 기반
react.when_some(maybe_user, fn(user) {
html.span_([react.text(user.name)])
})
```
### μ™ΈλΆ€ React μ»΄ν¬λ„ŒνŠΈ μ‚¬μš© (바인딩)
`.mjs` 파일 μž‘μ„± 없이 μ™ΈλΆ€ React 라이브러리λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.
**1. `bindings.json` μž‘μ„±:**
```json
{
"recharts": {
"components": ["PieChart", "Pie", "Cell", "Tooltip", "Legend"]
}
}
```
**2. `gleam run -m glendix/install` μ‹€ν–‰** (바인딩 μžλ™ 생성)
**3. 순수 Gleam으둜 μ‚¬μš©:**
```gleam
import glendix/binding
import glendix/react
import glendix/react/prop
fn recharts() { binding.module("recharts") }
pub fn my_chart(data) -> react.ReactElement {
react.component(
binding.resolve(recharts(), "PieChart"),
prop.new() |> prop.int("width", 400) |> prop.int("height", 300),
[
react.component(
binding.resolve(recharts(), "Pie"),
prop.new() |> prop.any("data", data) |> prop.string("dataKey", "value"),
[],
),
],
)
}
```
## Build Scripts
glendix에 λ‚΄μž₯된 λΉŒλ“œ 슀크립트둜, μœ„μ ― ν”„λ‘œμ νŠΈμ—μ„œ 별도 슀크립트 파일 없이 `gleam run -m`으둜 μ‹€ν–‰ν•œλ‹€.
| λͺ…λ Ήμ–΄ | μ„€λͺ… |
|--------|------|
| `gleam run -m glendix/install` | μ˜μ‘΄μ„± μ„€μΉ˜ + 바인딩 생성 (PM μžλ™ 감지) |
| `gleam run -m glendix/build` | ν”„λ‘œλ•μ…˜ λΉŒλ“œ (.mpk 생성) |
| `gleam run -m glendix/dev` | 개발 μ„œλ²„ (HMR, port 3000) |
| `gleam run -m glendix/start` | Mendix ν…ŒμŠ€νŠΈ ν”„λ‘œμ νŠΈ 연동 |
| `gleam run -m glendix/lint` | ESLint μ‹€ν–‰ |
| `gleam run -m glendix/lint_fix` | ESLint μžλ™ μˆ˜μ • |
| `gleam run -m glendix/release` | 릴리즈 λΉŒλ“œ |
νŒ¨ν‚€μ§€ λ§€λ‹ˆμ €λŠ” lock 파일 기반으둜 μžλ™ κ°μ§€λœλ‹€:
- `pnpm-lock.yaml` β†’ pnpm
- `bun.lockb` / `bun.lock` β†’ bun
- κΈ°λ³Έκ°’ β†’ npm
## Architecture
```
glendix/
react.gleam ← 핡심 νƒ€μž… + createElement
react_ffi.mjs ← React μ›μ‹œ ν•¨μˆ˜ (얇은 μ–΄λŒ‘ν„°)
react/
prop.gleam ← Props λΉŒλ”
hook.gleam ← React Hooks
event.gleam ← 이벀트 νƒ€μž…
html.gleam ← HTML νƒœκ·Έ (순수 Gleam)
mendix.gleam ← Mendix 핡심 νƒ€μž… + Props μ ‘κ·Όμž
mendix_ffi.mjs ← Mendix λŸ°νƒ€μž„ νƒ€μž… μ ‘κ·Ό (얇은 μ–΄λŒ‘ν„°)
mendix/
editable_value.gleam ← EditableValue
action.gleam ← ActionValue
dynamic_value.gleam ← DynamicValue
list_value.gleam ← ListValue + Sort + Filter
list_attribute.gleam ← List-linked νƒ€μž…
selection.gleam ← Selection
reference.gleam ← Reference
date.gleam ← JS Date 래퍼
big.gleam ← Big.js 래퍼
file.gleam ← File / Image
icon.gleam ← Icon
formatter.gleam ← ValueFormatter
filter.gleam ← FilterCondition λΉŒλ”
binding.gleam ← μ™ΈλΆ€ React μ»΄ν¬λ„ŒνŠΈ 바인딩 API
binding_ffi.mjs ← 바인딩 FFI (install μ‹œ μžλ™ ꡐ체)
cmd.gleam ← μ…Έ λͺ…λ Ήμ–΄ μ‹€ν–‰ + PM 감지 + 바인딩 생성
cmd_ffi.mjs ← Node.js child_process + fs FFI + 바인딩 생성
build.gleam ← λΉŒλ“œ 슀크립트
dev.gleam ← 개발 μ„œλ²„ 슀크립트
start.gleam ← Mendix 연동 슀크립트
install.gleam ← μ˜μ‘΄μ„± μ„€μΉ˜ + 바인딩 생성 슀크립트
release.gleam ← 릴리즈 λΉŒλ“œ 슀크립트
lint.gleam ← ESLint 슀크립트
lint_fix.gleam ← ESLint μžλ™ μˆ˜μ • 슀크립트
```
## Design Principles
- **FFIλŠ” 얇은 μ–΄λŒ‘ν„°μΌ 뿐이닀.** `react_ffi.mjs`λŠ” React μ›μ‹œ ν•¨μˆ˜, `mendix_ffi.mjs`λŠ” Mendix λŸ°νƒ€μž„ νƒ€μž… μ ‘κ·Όμžλ₯Ό λ…ΈμΆœν•  뿐, λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ€ μ „λΆ€ Gleam으둜 μž‘μ„±ν•œλ‹€.
- **Opaque type으둜 νƒ€μž… μ•ˆμ „μ„± 보μž₯.** `ReactElement`, `JsProps`, `EditableValue` λ“± JS 값을 Gleam의 opaque type으둜 감싸 잘λͺ»λœ 접근을 컴파일 νƒ€μž„μ— μ°¨λ‹¨ν•œλ‹€.
- **`undefined` ↔ `Option` μžλ™ λ³€ν™˜.** FFI κ²½κ³„μ—μ„œ JS `undefined`/`null`은 Gleam `None`으둜, 값이 있으면 `Some(value)`으둜 λ³€ν™˜λœλ‹€.
- **νŒŒμ΄ν”„λΌμΈ API.** PropsλŠ” `prop.new() |> prop.class("x") |> prop.on_click(handler)` νŒ¨ν„΄μœΌλ‘œ κ΅¬μ„±ν•œλ‹€.
- **Gleam νŠœν”Œ = JS λ°°μ—΄.** `#(a, b)` = `[a, b]`μ΄λ―€λ‘œ `useState`의 λ°˜ν™˜κ°’κ³Ό 직접 ν˜Έν™˜λœλ‹€.
## License
Apache-2.0