diff --git a/crates/oxc_angular_compiler/src/component/metadata.rs b/crates/oxc_angular_compiler/src/component/metadata.rs index 6802a0ead..deed550c0 100644 --- a/crates/oxc_angular_compiler/src/component/metadata.rs +++ b/crates/oxc_angular_compiler/src/component/metadata.rs @@ -85,6 +85,16 @@ impl AngularVersion { self.major >= 22 } + /// Check if this version emits control instructions at all (v21.2.0+). + /// + /// Angular's `specializeControlProperties` phase and the + /// `ɵɵcontrolCreate()`/`ɵɵcontrol()` instructions were introduced in + /// v21.2.0 for the `formField` control directive. Earlier versions never + /// emit them. + pub fn supports_control_instructions(&self) -> bool { + self.major > 21 || (self.major == 21 && self.minor >= 2) + } + /// Check if this version emits control instructions for the extended set of /// control properties (v22.0.0+). /// @@ -97,6 +107,20 @@ impl AngularVersion { self.major >= 22 } + /// Check if this version anchors `ɵɵcontrolCreate()` at the element's first + /// create instruction (v22.2.0+). + /// + /// Through v22.1.x, `findCreateInstruction` kept the *last* matching create + /// op, so `ɵɵcontrolCreate()` landed after `ElementEnd`/`ContainerEnd` (and + /// `Template` was a valid anchor). v22.2.0 switched to the *first* matching + /// op and narrowed the anchor kinds to `Element`, `ElementStart`, + /// `Container`, and `ContainerStart`, so the instruction sits right after + /// the element start (before listeners) and `ng-template` no longer gets + /// control instructions. + pub fn uses_first_control_anchor(&self) -> bool { + self.major > 22 || (self.major == 22 && self.minor >= 2) + } + /// Check if this version uses modern optional-chaining semantics (v22.0.0+). /// /// Angular v22 changed the safe-navigation operator (`?.`) in template diff --git a/crates/oxc_angular_compiler/src/pipeline/ingest.rs b/crates/oxc_angular_compiler/src/pipeline/ingest.rs index f0208ddd5..10d95cc2b 100644 --- a/crates/oxc_angular_compiler/src/pipeline/ingest.rs +++ b/crates/oxc_angular_compiler/src/pipeline/ingest.rs @@ -40,12 +40,12 @@ use crate::ir::expression::{ TwoWayBindingSetExpr, }; use crate::ir::ops::{ - BindingOp, ConditionalBranchCreateOp, ConditionalOp, ConditionalUpdateOp, ControlCreateOp, - CreateOp, CreateOpBase, DeclareLetOp, DeferOnOp, DeferOp, DeferWhenOp, ElementEndOp, - ElementStartOp, ExtractedAttributeOp, I18nAttributesOp, I18nEndOp, I18nPlaceholder, - I18nSlotHandle, I18nStartOp, IcuEndOp, IcuStartOp, InterpolateTextOp, ListenerOp, LocalRef, - ProjectionOp, RepeaterCreateOp, RepeaterOp, RepeaterVarNames, SlotId, StatementOp, StoreLetOp, - TemplateOp, TextOp, TwoWayListenerOp, UpdateOp, UpdateOpBase, XrefId, + BindingOp, ConditionalBranchCreateOp, ConditionalOp, ConditionalUpdateOp, CreateOp, + CreateOpBase, DeclareLetOp, DeferOnOp, DeferOp, DeferWhenOp, ElementEndOp, ElementStartOp, + ExtractedAttributeOp, I18nAttributesOp, I18nEndOp, I18nPlaceholder, I18nSlotHandle, + I18nStartOp, IcuEndOp, IcuStartOp, InterpolateTextOp, ListenerOp, LocalRef, ProjectionOp, + RepeaterCreateOp, RepeaterOp, RepeaterVarNames, SlotId, StatementOp, StoreLetOp, TemplateOp, + TextOp, TwoWayListenerOp, UpdateOp, UpdateOpBase, XrefId, }; use crate::output::ast::OutputExpression; use crate::pipeline::compilation::{AliasVariable, ContextVariable}; @@ -1113,47 +1113,6 @@ fn ingest_element<'a>( // Process local references let local_refs = ingest_references_owned(allocator, element.references); - // Check for a control property binding to create a ControlCreateOp, matching - // Angular v22's `specializeControlProperties`. The eligible properties and the - // binding kinds they accept are: - // - formField / formControl: `[..]` property binding - // - formControlName: property binding or static attribute - // - ngModel: property, two-way `[(ngModel)]`, or static attribute - // (v21 only emitted this for `formField`; v22 broadened it, notably to - // two-way `[(ngModel)]`, which now also emits `ɵɵcontrolCreate()`.) - use crate::ast::expression::BindingType; - // `formField` is the v21 baseline; the extended set (formControl/ - // formControlName/ngModel) was added in v22. Default to latest when unknown. - let extended_controls = - job.angular_version.map_or(true, |v| v.supports_extended_control_properties()); - let field_input_span = element - .inputs - .iter() - .find_map(|input| { - let eligible = match input.name.as_str() { - "formField" => input.binding_type == BindingType::Property, - "formControl" if extended_controls => input.binding_type == BindingType::Property, - "formControlName" if extended_controls => { - input.binding_type == BindingType::Property - } - "ngModel" if extended_controls => { - matches!(input.binding_type, BindingType::Property | BindingType::TwoWay) - } - _ => false, - }; - eligible.then_some(input.source_span) - }) - .or_else(|| { - // Static attributes (`formControlName="name"`, `ngModel`) -> Attribute op (v22+). - if !extended_controls { - return None; - } - element.attributes.iter().find_map(|attr| { - matches!(attr.name.as_str(), "formControlName" | "ngModel") - .then_some(attr.source_span) - }) - }); - // Always create ElementStart/ElementEnd pairs, even for void/self-closing elements. // The empty_elements phase will collapse them to Element when appropriate. // This matches TypeScript Angular's ingest.ts which always creates start/end pairs. @@ -1311,18 +1270,6 @@ fn ingest_element<'a>( if let Some(view) = job.view_mut(view_xref) { view.create.push(end_op); } - - // We want to ensure that the controlCreateOp is after the ops that create the element. - // Ported from Angular's ingest.ts lines 319-327. - // If the element has a [field] property binding, add ControlCreateOp. - // This is used for form control bindings that require synchronization. - if let Some(span) = field_input_span { - if let Some(view) = job.view_mut(view_xref) { - view.create.push(CreateOp::ControlCreate(ControlCreateOp { - base: CreateOpBase { source_span: Some(span), ..Default::default() }, - })); - } - } } /// Ingests static attributes from R3TextAttribute, preserving i18n metadata. diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/binding_specialization.rs b/crates/oxc_angular_compiler/src/pipeline/phases/binding_specialization.rs index d2dbad0b4..03829a47e 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/binding_specialization.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/binding_specialization.rs @@ -28,8 +28,8 @@ use crate::ast::expression::{AbsoluteSourceSpan, AngularExpression, EmptyExpr, P use crate::ir::enums::{AnimationBindingKind, BindingKind}; use crate::ir::expression::IrExpression; use crate::ir::ops::{ - AnimationBindingOp, AttributeOp, ControlOp, CreateOp, DomPropertyOp, PropertyOp, - TwoWayPropertyOp, UpdateOp, UpdateOpBase, XrefId, + AnimationBindingOp, AttributeOp, CreateOp, DomPropertyOp, PropertyOp, TwoWayPropertyOp, + UpdateOp, UpdateOpBase, XrefId, }; use crate::pipeline::compilation::{ ComponentCompilationJob, HostBindingCompilationJob, TemplateCompilationMode, @@ -88,10 +88,6 @@ fn split_ns_name(name: &str) -> (Option<&str>, &str) { pub fn specialize_bindings(job: &mut ComponentCompilationJob<'_>) { let allocator = job.allocator; let mode = job.mode; - // v22 broadened control-property specialization beyond `formField`. Default to - // the latest behaviour when the target version is unknown. - let extended_controls = - job.angular_version.map_or(true, |v| v.supports_extended_control_properties()); // First pass: Build element map from create operations let mut elements: FxHashMap = FxHashMap::default(); @@ -116,14 +112,12 @@ pub fn specialize_bindings(job: &mut ComponentCompilationJob<'_>) { let mut all_non_bindable: Vec = Vec::new(); // Process root view - let root_non_bindable = - specialize_in_view(&mut job.root.update, &allocator, &elements, mode, extended_controls); + let root_non_bindable = specialize_in_view(&mut job.root.update, &allocator, &elements, mode); all_non_bindable.extend(root_non_bindable); // Process embedded views for view in job.views.values_mut() { - let view_non_bindable = - specialize_in_view(&mut view.update, &allocator, &elements, mode, extended_controls); + let view_non_bindable = specialize_in_view(&mut view.update, &allocator, &elements, mode); all_non_bindable.extend(view_non_bindable); } @@ -174,7 +168,7 @@ fn get_element_info(op: &CreateOp<'_>) -> Option<(XrefId, ElementInfo)> { } /// Creates a placeholder expression. -fn create_placeholder_expression<'a>( +pub(super) fn create_placeholder_expression<'a>( allocator: &'a oxc_allocator::Allocator, ) -> Box<'a, IrExpression<'a>> { let empty_expr = AngularExpression::Empty(Box::new_in( @@ -191,7 +185,6 @@ fn specialize_in_view<'a>( allocator: &'a oxc_allocator::Allocator, _elements: &FxHashMap, mode: TemplateCompilationMode, - extended_controls: bool, ) -> Vec { // Track ops to remove (ngNonBindable) let mut to_remove: Vec>> = Vec::new(); @@ -307,46 +300,6 @@ fn specialize_in_view<'a>( }); cursor.replace_current(new_op); } - } else if name.as_str() == "formField" - || (extended_controls - && matches!( - name.as_str(), - "formControl" | "formControlName" | "ngModel" - )) - { - // A control property (`[formField]`, `[formControl]`, - // `[formControlName]`, `[ngModel]`) still binds as a regular property, - // but Angular also emits a separate control instruction - // (`ɵɵcontrol()`) after the property update. `formField` is the v21 - // baseline; the rest were added in v22 (`extended_controls`). - if let Some(UpdateOp::Binding(binding)) = cursor.current_mut() { - let expression = std::mem::replace( - &mut binding.expression, - create_placeholder_expression(allocator), - ); - 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(property_op); - cursor.insert_after(control_op); - } } else { // Regular property binding // Note: In host binding mode, this would become DomPropertyOp @@ -390,20 +343,6 @@ fn specialize_in_view<'a>( sanitizer: None, }); cursor.replace_current(new_op); - // Angular v22: a two-way `[(ngModel)]` is a control property, so it - // also emits a control update instruction (`ɵɵcontrol()`) after the - // two-way property update (paired with the `ɵɵcontrolCreate()` added - // during ingest). Gated to v22+ via `extended_controls`. - if extended_controls && name.as_str() == "ngModel" { - let control_op = UpdateOp::Control(ControlOp { - base: UpdateOpBase { source_span, ..Default::default() }, - target, - name, - expression: create_placeholder_expression(allocator), - security_context, - }); - cursor.insert_after(control_op); - } } } BindingKind::I18n | BindingKind::ClassName | BindingKind::StyleProperty => { diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/mod.rs b/crates/oxc_angular_compiler/src/pipeline/phases/mod.rs index 896416b0a..7af52ad92 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/mod.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/mod.rs @@ -68,6 +68,7 @@ mod resolve_i18n_expression_placeholders; mod resolve_names; mod resolve_sanitizers; mod save_restore_view; +mod specialize_control_properties; mod store_let_optimization; mod strip_nonrequired_parentheses; mod style_binding_specialization; @@ -174,6 +175,13 @@ pub static PHASES: &[Phase] = &[ run_host: Some(binding_specialization::specialize_bindings_for_host), name: "specializeBindings", }, + // Phase 9b: specializeControlProperties (Template only) + Phase { + kind: CompilationJobKind::Template, + run: specialize_control_properties::specialize_control_properties, + run_host: None, + name: "specializeControlProperties", + }, // Phase 10: convertAnimations (Both) Phase { kind: CompilationJobKind::Both, diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/specialize_control_properties.rs b/crates/oxc_angular_compiler/src/pipeline/phases/specialize_control_properties.rs new file mode 100644 index 000000000..9aac47826 --- /dev/null +++ b/crates/oxc_angular_compiler/src/pipeline/phases/specialize_control_properties.rs @@ -0,0 +1,205 @@ +//! Specialize control properties phase. +//! +//! For each eligible control binding (`formField`, and on v22+ `formControl`, +//! `formControlName`, `ngModel`) in a view's update list, this phase: +//! 1. inserts a `ControlCreateOp` into the create list, anchored to the +//! element/container create op that owns the binding target, and +//! 2. inserts an `UpdateOp::Control` right after the binding update op. +//! +//! Reification emits `ɵɵcontrolCreate()` / `ɵɵcontrol()` for these ops. +//! +//! Version differences (see `AngularVersion` predicates): +//! - `< v21.2`: no control instructions at all. +//! - `v21.2`–`v22.1.x`: `ɵɵcontrolCreate()` is anchored after the *last* +//! create op for the target (`ElementEnd`/`ContainerEnd`/`Template` count), +//! so it lands just after the element ends. +//! - `v22.2+`: `ɵɵcontrolCreate()` is anchored after the *first* create op +//! for the target (only `Element`/`ElementStart`/`Container`/`ContainerStart` +//! count), so it lands right after the element start. `ng-template` stops +//! being a valid anchor, which drops control instructions there entirely. +//! +//! Ported from Angular's `template/pipeline/src/phases/specialize_control_properties.ts`. + +use std::ptr::NonNull; + +use oxc_span::Span; +use oxc_str::Ident; + +use crate::ast::r3::SecurityContext; +use crate::ir::enums::OpKind; +use crate::ir::list::{CreateOpList, UpdateOpList}; +use crate::ir::ops::{ + ControlCreateOp, ControlOp, CreateOp, CreateOpBase, Op, UpdateOp, UpdateOpBase, XrefId, +}; +use crate::pipeline::compilation::ComponentCompilationJob; + +use super::binding_specialization::create_placeholder_expression; + +/// Runs the specialize-control-properties phase for a template compilation job. +/// +/// Corresponds to Angular's `specializeControlProperties` (template jobs only). +pub fn specialize_control_properties(job: &mut ComponentCompilationJob<'_>) { + // Control instructions (`formField` controls) were introduced in v21.2. + let Some(version) = job.angular_version else { + // Unknown target: behave like the latest Angular. + let extended = true; + let first_anchor = true; + run_job(job, extended, first_anchor); + return; + }; + if !version.supports_control_instructions() { + return; + } + let extended = version.supports_extended_control_properties(); + let first_anchor = version.uses_first_control_anchor(); + run_job(job, extended, first_anchor); +} + +fn run_job(job: &mut ComponentCompilationJob<'_>, extended: bool, first_anchor: bool) { + let allocator = job.allocator; + for view in job.all_views_mut() { + process_view(&mut view.create, &mut view.update, allocator, extended, first_anchor); + } +} + +/// Metadata captured from an eligible update op. +struct Eligible<'a> { + /// Pointer to the binding update op; `Control` is inserted after it. + update_ptr: NonNull>, + /// Binding target (element/container/template xref). + target: XrefId, + /// Binding name (`formField`, `formControl`, `formControlName`, `ngModel`). + name: Ident<'a>, + /// Source span copied to both inserted ops. + source_span: Option, + /// Security context copied to the `Control` op. + security_context: SecurityContext, +} + +fn process_view<'a>( + create: &mut CreateOpList<'a>, + update: &mut UpdateOpList<'a>, + allocator: &'a oxc_allocator::Allocator, + extended: bool, + first_anchor: bool, +) { + // Collect eligible update ops first so we can mutate both lists freely. + let mut eligible: Vec> = Vec::new(); + let mut ptr = update.head_ptr(); + while let Some(op_ptr) = ptr { + // SAFETY: pointers come from a live op list and outlive this loop. + let op = unsafe { op_ptr.as_ref() }; + if let Some(e) = eligible_entry(op_ptr, op, extended) { + eligible.push(e); + } + ptr = op.next(); + } + + for entry in eligible { + let Some(anchor) = find_control_anchor(create, entry.target, first_anchor) else { + // v22+: no valid anchor (e.g. `ng-template` targets) drops both + // instructions. v21.2 upstream throws; we skip instead. + continue; + }; + let span = entry.source_span; + // SAFETY: `anchor` is a live node of `create`; inserting after it keeps + // the list well-formed. + unsafe { + create.insert_after( + anchor, + CreateOp::ControlCreate(ControlCreateOp { + base: CreateOpBase { source_span: span, ..Default::default() }, + }), + ); + } + // SAFETY: `entry.update_ptr` is a live node of `update`. + unsafe { + update.insert_after( + entry.update_ptr, + UpdateOp::Control(ControlOp { + base: UpdateOpBase { source_span: span, ..Default::default() }, + target: entry.target, + name: entry.name, + expression: create_placeholder_expression(allocator), + security_context: entry.security_context, + }), + ); + } + } +} + +/// Returns an `Eligible` entry when `op` is a control-eligible binding. +/// +/// Mirrors Angular's `ELIGIBLE_CONTROL_PROPERTIES`: +/// - `formField`: Property +/// - `formControl`: Property (v22+) +/// - `formControlName`: Property or Attribute (v22+) +/// - `ngModel`: Attribute, Property, or TwoWayProperty (v22+) +fn eligible_entry<'a>( + update_ptr: NonNull>, + op: &UpdateOp<'a>, + extended: bool, +) -> Option> { + let kind = op.kind(); + let (name, target, source_span, security_context) = match op { + UpdateOp::Property(p) => (p.name, p.target, p.base.source_span, p.security_context), + UpdateOp::Attribute(a) => (a.name, a.target, a.base.source_span, a.security_context), + UpdateOp::TwoWayProperty(t) => (t.name, t.target, t.base.source_span, t.security_context), + _ => return None, + }; + let eligible = match name.as_str() { + "formField" => kind == OpKind::Property, + "formControl" => extended && kind == OpKind::Property, + "formControlName" => extended && matches!(kind, OpKind::Property | OpKind::Attribute), + "ngModel" => { + extended + && matches!(kind, OpKind::Attribute | OpKind::Property | OpKind::TwoWayProperty) + } + _ => false, + }; + eligible.then_some(Eligible { update_ptr, target, name, source_span, security_context }) +} + +/// Finds the create-op anchor for `ɵɵcontrolCreate()`. +/// +/// Pre-v22.2 upstream keeps the *last* relevant create op for the target +/// (element/container ends and templates included). v22.2+ keeps the *first* +/// and only accepts element/container starts (or their collapsed forms). +fn find_control_anchor<'a>( + create: &CreateOpList<'a>, + target: XrefId, + first_anchor: bool, +) -> Option>> { + let mut last: Option>> = None; + let mut ptr = create.head_ptr(); + while let Some(op_ptr) = ptr { + // SAFETY: pointers come from a live op list and outlive this loop. + let op = unsafe { op_ptr.as_ref() }; + if let Some(xref) = anchor_xref(op, first_anchor) + && xref == target + { + if first_anchor { + return Some(op_ptr); + } + last = Some(op_ptr); + } + + ptr = op.next(); + } + last +} + +/// Returns the xref of a create op that can anchor `ɵɵcontrolCreate()`, +/// restricted to the version-appropriate kind set. +fn anchor_xref(op: &CreateOp<'_>, first_anchor: bool) -> Option { + match op { + CreateOp::Element(e) => Some(e.xref), + CreateOp::ElementStart(e) => Some(e.xref), + CreateOp::Container(c) => Some(c.xref), + CreateOp::ContainerStart(c) => Some(c.xref), + CreateOp::ElementEnd(e) if !first_anchor => Some(e.xref), + CreateOp::ContainerEnd(c) if !first_anchor => Some(c.xref), + CreateOp::Template(t) if !first_anchor => Some(t.xref), + _ => None, + } +} diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__for_two_way_binding_with_index.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__for_two_way_binding_with_index.snap index cb3434599..2defa00ba 100644 --- a/crates/oxc_angular_compiler/tests/snapshots/integration_test__for_two_way_binding_with_index.snap +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__for_two_way_binding_with_index.snap @@ -7,6 +7,7 @@ function TestComponent_For_1_Template(rf,ctx) { const _r1 = i0.ɵɵgetCurrentView(); i0.ɵɵtext(0," "); i0.ɵɵelementStart(1,"input",0); + i0.ɵɵcontrolCreate(); i0.ɵɵtwoWayListener("ngModelChange",function TestComponent_For_1_Template_input_ngModelChange_1_listener($event) { const $index_r2 = i0.ɵɵrestoreView(_r1).$index; const ctx_r2 = i0.ɵɵnextContext(); @@ -14,7 +15,6 @@ function TestComponent_For_1_Template(rf,ctx) { return i0.ɵɵresetView($event); }); i0.ɵɵelementEnd(); - i0.ɵɵcontrolCreate(); i0.ɵɵtext(2," "); } if ((rf & 2)) { diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__todo_list.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__todo_list.snap index cdc0a7019..50121af8e 100644 --- a/crates/oxc_angular_compiler/tests/snapshots/integration_test__todo_list.snap +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__todo_list.snap @@ -10,13 +10,13 @@ function TodoListComponent_For_7_Template(rf,ctx) { i0.ɵɵelementStart(1,"div"); i0.ɵɵtext(2,"\n "); i0.ɵɵelementStart(3,"input",1); + i0.ɵɵcontrolCreate(); i0.ɵɵtwoWayListener("ngModelChange",function TodoListComponent_For_7_Template_input_ngModelChange_3_listener($event) { const todo_r2 = i0.ɵɵrestoreView(_r1).$implicit; (i0.ɵɵtwoWayBindingSet(todo_r2.done,$event) || (todo_r2.done = $event)); return i0.ɵɵresetView($event); }); i0.ɵɵelementEnd(); - i0.ɵɵcontrolCreate(); i0.ɵɵtext(4,"\n "); i0.ɵɵelementStart(5,"span"); i0.ɵɵtext(6); @@ -62,6 +62,7 @@ function TodoListComponent_Template(rf,ctx) { i0.ɵɵelementEnd(); i0.ɵɵtext(3,"\n "); i0.ɵɵelementStart(4,"input",0); + i0.ɵɵcontrolCreate(); i0.ɵɵtwoWayListener("ngModelChange",function TodoListComponent_Template_input_ngModelChange_4_listener($event) { (i0.ɵɵtwoWayBindingSet(ctx.newTodo,$event) || (ctx.newTodo = $event)); return $event; @@ -70,7 +71,6 @@ function TodoListComponent_Template(rf,ctx) { return ctx.addTodo(); }); i0.ɵɵelementEnd(); - i0.ɵɵcontrolCreate(); i0.ɵɵtext(5,"\n "); i0.ɵɵrepeaterCreate(6,TodoListComponent_For_7_Template,12,4,null,null,_forTrack0, false,TodoListComponent_ForEmpty_8_Template,4,0); diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__two_way_binding.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__two_way_binding.snap index 14f4c5254..724e6d342 100644 --- a/crates/oxc_angular_compiler/tests/snapshots/integration_test__two_way_binding.snap +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__two_way_binding.snap @@ -5,12 +5,12 @@ expression: js function TestComponent_Template(rf,ctx) { if ((rf & 1)) { i0.ɵɵelementStart(0,"input",0); + i0.ɵɵcontrolCreate(); i0.ɵɵtwoWayListener("ngModelChange",function TestComponent_Template_input_ngModelChange_0_listener($event) { (i0.ɵɵtwoWayBindingSet(ctx.name,$event) || (ctx.name = $event)); return $event; }); i0.ɵɵelementEnd(); - i0.ɵɵcontrolCreate(); } if ((rf & 2)) { i0.ɵɵtwoWayProperty("ngModel",ctx.name);