Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -237,25 +237,6 @@ fn process_view_attributes<'a>(
};
extracted_attrs.push((twp_op.target, extracted));
}
UpdateOp::Control(control_op) => {
// Control bindings (e.g. [field]="...") also generate extracted
// attributes for directive matching, similar to Property bindings.
// Ported from Angular's attribute_extraction.ts lines 58-73.
let extracted = ExtractedAttributeOp {
base: CreateOpBase::default(),
target: control_op.target,
binding_kind: BindingKind::Property,
namespace: None,
name: control_op.name.clone(),
value: None, // Control bindings don't copy the expression
security_context: control_op.security_context,
truthy_expression: false,
i18n_context: None,
i18n_message: None,
trusted_value_fn: None, // Set by resolve_sanitizers phase
};
extracted_attrs.push((control_op.target, extracted));
}
// StyleProp and ClassProp bindings:
// In Angular TypeScript, these are only extracted in compatibility mode
// (TemplateDefinitionBuilder) when the expression is empty. We don't support
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
//! Special cases:
//! - `ngNonBindable` attribute: marks element and removes binding
//! - `animate.*` attributes: convert to animation bindings
//! - `field` property: convert to control binding
//! - `formField` property: emit both a regular property binding and a control binding
//!
//! Ported from Angular's `template/pipeline/src/phases/binding_specialization.ts`.

Expand Down Expand Up @@ -301,20 +301,35 @@ fn specialize_in_view<'a>(
cursor.replace_current(new_op);
}
} else if name.as_str() == "formField" {
// Check for special "formField" property (control binding)
// [formField] still binds as a regular property, but Angular also emits
// a separate control instruction after the property update.
if let Some(UpdateOp::Binding(binding)) = cursor.current_mut() {
let expression = std::mem::replace(
&mut binding.expression,
create_placeholder_expression(allocator),
);
let new_op = UpdateOp::Control(ControlOp {
let property_op = UpdateOp::Property(PropertyOp {
base: UpdateOpBase { source_span, ..Default::default() },
target,
name: binding.name.clone(),
expression,
is_host: false, // Template mode
security_context,
sanitizer: None,
is_structural: false,
i18n_context: None,
i18n_message: binding.i18n_message,
binding_kind,
});
let control_op = UpdateOp::Control(ControlOp {
base: UpdateOpBase { source_span, ..Default::default() },
target,
name: binding.name.clone(),
expression: create_placeholder_expression(allocator),
security_context,
});
cursor.replace_current(new_op);
cursor.replace_current(property_op);
cursor.insert_after(control_op);
}
} else {
// Regular property binding
Expand Down
5 changes: 1 addition & 4 deletions crates/oxc_angular_compiler/src/pipeline/phases/reify/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1073,10 +1073,7 @@ fn reify_update_op<'a>(
let expr = convert_ir_expression(allocator, &anim.expression, expressions, root_xref);
Some(create_animation_binding_stmt(allocator, &anim.name, expr))
}
UpdateOp::Control(ctrl) => {
let expr = convert_ir_expression(allocator, &ctrl.expression, expressions, root_xref);
Some(create_control_stmt(allocator, expr, &ctrl.name))
}
UpdateOp::Control(_) => Some(create_control_stmt(allocator)),
UpdateOp::Variable(var) => {
// Emit variable declaration with initializer for update phase
// All Variable ops use `const` (StmtModifier::Final), matching Angular's reify.ts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,26 +377,11 @@ pub fn create_animation_binding_stmt<'a>(

/// Creates a control binding call statement (ɵɵcontrol).
///
/// The control instruction takes:
/// - expression: The expression to evaluate for the control value
/// - name: The property name as a string literal
/// - sanitizer: Optional sanitizer (only if not null)
///
/// Note: Unlike property() which takes (name, expression), control() takes (expression, name).
/// Ported from Angular's `control()` in `instruction.ts` lines 598-614.
pub fn create_control_stmt<'a>(
allocator: &'a oxc_allocator::Allocator,
value: OutputExpression<'a>,
name: &Ident<'a>,
) -> OutputStatement<'a> {
let mut args = OxcVec::new_in(allocator);
args.push(value);
args.push(OutputExpression::Literal(Box::new_in(
LiteralExpr { value: LiteralValue::String(name.clone()), source_span: None },
allocator,
)));
// Note: sanitizer would be pushed here if not null, but it's always null for ControlOp
create_instruction_call_stmt(allocator, Identifiers::CONTROL, args)
/// Angular's control update instruction takes no arguments. The `[formField]`
/// value is written through the regular property instruction, and `ɵɵcontrol()`
/// performs the form-control synchronization work separately.
pub fn create_control_stmt<'a>(allocator: &'a oxc_allocator::Allocator) -> OutputStatement<'a> {
create_instruction_call_stmt(allocator, Identifiers::CONTROL, OxcVec::new_in(allocator))
}

/// Creates an ɵɵprojectionDef() call statement from a pre-built R3 def expression.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,10 @@ fn vars_used_by_update_op(
1
}
UpdateOp::Control(_) => {
// Control bindings use 2 slots (one for value, one for bound states)
2
// Angular's ControlOp does not implement ConsumesVarsTrait, so it does not
// contribute any top-level variable slots. The bound [formField] value is
// accounted for by the PropertyOp that precedes it.
0
}
UpdateOp::Conditional(_) => 1,
UpdateOp::StoreLet(_) => 1,
Expand Down
Loading
Loading