Skip to main content

Fragment Constraints

Fragment constraints let AppConfig reposition and resize fragment children after the fragment is mounted, data is mapped, computed values are evaluated, or view status changes. Use them for layout relationships that should stay in metadata instead of callbacks, such as pinning a label to a poster, filling a background between two edges, keeping artwork at an aspect ratio, or anchoring a badge to the fragment cell.

The typical authoring form is an inline constraint binding on the field that the constraint writes. AppConfig compiles those inline bindings into the same _dataMap.constraints rules used by the runtime.

Inline Constraint Bindings

Use {{constraint.*(...)}} in fragment layout fields:

{
"id": "titleLabel",
"subType": "Label",
"text": "{{data.title}}",
"width": "{{constraint.fillX(left, cell.right, 0, 40, { priority: 10, min: 120, max: 520 })}}",
"translation": [
"{{constraint.pin(left, poster.right, 24)}}",
"{{constraint.pin(top, poster.top, 0)}}"
]
}

During AppConfig resolution, those authored fields are removed from the emitted static SG node style and appended to _dataMap.constraints:

{
"_dataMap": {
"constraints": [
{ "viewId": "titleLabel", "property": "width", "fn": "fillX", "args": ["titleLabel", "left", "cell", "right", 0, 40], "options": { "priority": 10, "min": 120, "max": 520 } },
{ "viewId": "titleLabel", "property": "x", "fn": "pin", "args": ["poster", "left", "right", 24] },
{ "viewId": "titleLabel", "property": "y", "fn": "pin", "args": ["poster", "top", "top", 0] }
]
}
}

translation[0] maps to constraint property x; translation[1] maps to y. If both translation entries are constraint bindings, AppConfig removes translation from the emitted SG style. If only one axis is constrained, AppConfig preserves the other literal axis and writes 0 for the constrained axis until the constraint pass runs.

Inline constraint bindings are compile-time metadata. AppConfig parses them during style resolution, stores the resulting rules on _dataMap.constraints, and the runtime uses those parsed rules without reparsing the inline expression. Use computed values when a constraint needs a numeric value derived from data, focus, or cell size.

Inline constraint bindings can include a named options object as the final argument:

{{constraint.fillX(cell.left, cell.right, 18, 18, { priority: 10, min: 120, max: 520 })}}
{{constraint.fillY(cell.top, cell.bottom, 0, 20, { min: 80 })}}
{{constraint.aspectRatio(1.777, "width", { max: 360 })}}

Legacy numeric priority tails still parse for compatibility:

{{constraint.pin(left, poster.right, 24, 100)}}
{{constraint.fillX(cell.left, cell.right, 0, 40, 100)}}

When the rig or editor updates and exports inline constraint expressions, prefer named options:

{{constraint.fillX(cell.left, cell.right, 0, 40, { priority: 100 })}}

Inline Forms

pin

"{{constraint.pin(selfEdge, target.edge, margin = 0, options?)}}"

pin moves one edge or center of the current view to another view or cell edge. The authored field determines the output axis: translation[0] or x writes x; translation[1] or y writes y.

{
"translation": [
"{{constraint.pin(left, poster.right, 24, 100)}}",
"{{constraint.pin(top, poster.top, 0)}}"
]
}

The first binding compiles to:

{ "viewId": "titleLabel", "property": "x", "fn": "pin", "args": ["poster", "left", "right", 24], "options": { "priority": 100 } }

fillX

"{{constraint.fillX(selfLeftEdge, rightTarget.edge, leftMargin = 0, rightMargin = 0, options?)}}"

fillX sets the current view's horizontal position and width between its own left-side edge and a right target edge. It is valid on width, translation[0], or x, and compiles to property width.

{
"width": "{{constraint.fillX(left, cell.right, 0, 40)}}"
}

This compiles to:

{ "viewId": "titleLabel", "property": "width", "fn": "fillX", "args": ["titleLabel", "left", "cell", "right", 0, 40] }

fillY

"{{constraint.fillY(selfTopEdge, bottomTarget.edge, topMargin = 0, bottomMargin = 0, options?)}}"

fillY sets the current view's vertical position and height between its own top-side edge and a bottom target edge. It is valid on height, translation[1], or y, and compiles to property height.

inset

"{{constraint.inset(target, left, top, right, bottom, options?)}}"

inset places the current view inside another view or cell. It writes x, y, width, and height at runtime. Negative inset values are allowed for background padding or outset behavior.

Inline inset can be authored on any layout field, but AppConfig emits one inset rule for the view and removes the authored field value.

aspectRatio

"{{constraint.aspectRatio(ratio, sourceDimension = \"width\", options?)}}"

aspectRatio derives one size axis from the other. On height, it usually derives height from width. Pass "height" as the source dimension when deriving width from height.

{
"height": "{{constraint.aspectRatio(1.7777777778)}}",
"width": "{{constraint.aspectRatio(1.7777777778, \"height\")}}"
}

Visual Examples

Each diagram shows the same constraint applied against two different cell sizes.

pin

pin keeps one edge of a view attached to another edge plus a margin. In this example the title remains attached to poster.right + 20 even when the cell gets wider.

Pin constraint example

fillX

fillX writes both x and width by filling the horizontal space between two edges. In this example the label stretches as the cell width grows, while the left and right margins stay fixed.

FillX constraint example

fillY

fillY writes both y and height by filling the vertical space between two edges. In this example the rail grows taller as the cell height grows.

FillY constraint example

inset

inset copies another view's rectangle and applies left, top, right, and bottom insets. Negative inset values create an outset, which is useful for padded backgrounds behind text.

Inset constraint example

aspectRatio

aspectRatio calculates the missing dimension from the current width or height. In this example the poster height is computed from its width using a 16:9 ratio.

Aspect ratio constraint example

Explicit Constraint Rules

Inline constraints compile into explicit rules. You can still write those rules directly in _dataMap.constraints when generated config, migration code, or a tooling pipeline needs the lower-level representation.

Each rule has this shape:

{
viewId: 'titleLabel',
property: 'x',
fn: 'pin',
args: ['poster', 'left', 'right', 24],
options: { priority: 100 },
}
  • viewId: child view to update.
  • property: target field for the rule. Supported values are x, y, width, and height.
  • fn: constraint function. Supported values are pin, fillX, fillY, inset, and aspectRatio.
  • args: function-specific arguments. References use child ids or cell for the fragment host bounds.
  • options: optional constraint metadata. Supported keys are priority, min, and max.
{
"cells": {
"responsiveHero": {
"$supportsDataMap": true,
"width": 900,
"height": 340,
"views": {
"base": [
{ "id": "poster", "subType": "Poster", "width": 420, "uri": "{{data.imageUrl}}" },
{ "id": "titleLabel", "subType": "Label", "height": 42, "text": "{{data.title}}" },
{ "id": "descriptionLabel", "subType": "Label", "height": 96, "text": "{{data.description}}" },
{ "id": "badge", "subType": "Rectangle", "width": 48, "height": 48 }
],
"normal": {}
},
"_dataMap": {
"view": {},
"data": {},
"fn": {},
"constraints": [
{ "viewId": "poster", "property": "height", "fn": "aspectRatio", "args": [1.7777777778] },
{ "viewId": "titleLabel", "property": "x", "fn": "pin", "args": ["poster", "left", "right", 30] },
{ "viewId": "titleLabel", "property": "y", "fn": "pin", "args": ["poster", "top", "top", 0] },
{ "viewId": "descriptionLabel", "property": "x", "fn": "pin", "args": ["titleLabel", "left", "left", 0] },
{ "viewId": "descriptionLabel", "property": "width", "fn": "fillX", "args": ["descriptionLabel", "left", "cell", "right", 0, 40], "options": { "priority": 10, "min": 120, "max": 520 } },
{ "viewId": "descriptionLabel", "property": "y", "fn": "pin", "args": ["titleLabel", "top", "bottom", 18] },
{ "viewId": "badge", "property": "x", "fn": "pin", "args": ["cell", "right", "right", -24] },
{ "viewId": "badge", "property": "y", "fn": "pin", "args": ["cell", "bottom", "bottom", -24] }
]
}
}
}
}

Explicit rule function args:

  • pin: [targetRef, sourceEdge, targetEdge, margin]
  • fillX: [leftRef, leftEdge, rightRef, rightEdge, leftMargin, rightMargin]
  • fillY: [topRef, topEdge, bottomRef, bottomEdge, topMargin, bottomMargin]
  • inset: [ref, left, top, right, bottom]
  • aspectRatio: [ratio] to derive height from width, or [ratio, "height"] to derive width from height

References And Edges

Supported edges are left, right, top, bottom, centerX, centerY, width, height, x, and y.

Inline constraints use target.edge references, such as poster.right or cell.bottom. Explicit rules split the target id and edge into separate args.

Use cell when a rule needs the FragmentView, CollectionView cell, or row header host bounds rather than another child view. Child-to-child constraints must reference views with the same parent.

Priority And Ordering

AppConfig sorts inline and explicit constraints together by options.priority ascending, then by original author order. Missing priority defaults to 0. Explicit constraints that still use a legacy top-level priority are normalized to options.priority during AppConfig parsing.

Use priority only when one constraint depends on another rule having already updated a view. Most fragments should not need priorities; keep related layout rules in natural authoring order when possible.

{
"_dataMap": {
"constraints": [
{ "viewId": "view1", "property": "x", "fn": "pin", "args": ["cell", "left", "left", 10], "options": { "priority": 50 } }
]
},
"views": [
{
"id": "view1",
"width": "{{constraint.fillX(left, cell.right, 0, 40)}}",
"translation": ["{{constraint.pin(left, poster.right, 24, { priority: 100 })}}", 20]
}
]
}

The fillX rule runs first with priority 0, the explicit rule runs second with priority 50, and the focused pin rule runs last with priority 100.

Runtime Semantics

Constraints live on the fragment data map as constraints. They are applied by CollectionView cells, header fragments, and FragmentView. FragmentView also applies explicit constraints when the fragment does not use $supportsDataMap.

The runtime reads SG bounds through measurement APIs and writes SG fields. x and y rules update translation[0] and translation[1]; width and height rules update those fields directly.

options.min and options.max clamp the computed output value for the rule's target property after constraint evaluation. They do not clamp individual arguments or margins. Bounds apply only to rule.property: a width rule clamps width, not the x value also emitted by fillX; a height rule clamps height, not the y value also emitted by fillY. Rules targeting x or y can also be bounded, but those bounds are not treated as dimension-only.

Hidden views resolve to zero width and height. Constraint evaluation without min or max does not clamp negative dimensions: inverted anchors, oversized margins, or inset values larger than the target bounds are authoring errors and remain visible during development.

Validation

AppConfig rejects inline constraints when:

  • the view has no id
  • the constraint expression is malformed
  • the function name is unknown
  • a target reference is not shaped like viewId.edge
  • an edge is unsupported
  • the constraint is authored on an invalid field
  • a numeric argument is missing or non-numeric
  • the options object is not the final inline argument
  • an option key is not one of priority, min, or max
  • an option value is missing or non-numeric
  • priority is present but is not an integer
  • both min and max are present and min > max

Error messages include the view id and original inline expression so the failing field can be found quickly.

The FragmentConstraintEditor exposes priority, min, and max controls. It exports named inline options and reconciles edited bounds so min does not exceed max.