Skip to content
This repository has been archived by the owner on Sep 16, 2022. It is now read-only.

Commit

Permalink
feature(templates): Support event tearoffs in AngularDart templates.
Browse files Browse the repository at this point in the history
Closes #1437

PiperOrigin-RevId: 202550856
  • Loading branch information
alorenzen authored and matanlurey committed Jun 29, 2018
1 parent c562ea9 commit d72bcef
Show file tree
Hide file tree
Showing 9 changed files with 295 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class ViewTestFooComponent0 extends AppView<import1.TestFooComponent> {
_el_0.append(_text_1);
_el_0.addEventListener('click', eventHandler1(_ChildDirective_0_5.instance.handleClick));
_el_0.addEventListener('keypress', eventHandler1(_ChildDirective_0_5.instance.handleKeyPress));
final subscription_0 = _ChildDirective_0_5.instance.trigger.listen(eventHandler1(_handle_trigger_0_0));
final subscription_0 = _ChildDirective_0_5.instance.trigger.listen(eventHandler1(ctx.onTrigger));
final subscription_1 = _DirectiveWithOutput_0_6.eventXyz.listen(eventHandler1(_ChildDirective_0_5.instance.handleXyzEventFromOtherDirective));
init(const [], [subscription_0, subscription_1]);
return null;
Expand All @@ -67,10 +67,6 @@ class ViewTestFooComponent0 extends AppView<import1.TestFooComponent> {
}
_ChildDirective_0_5.detectHostChanges(this, _el_0);
}

void _handle_trigger_0_0($event) {
ctx.onTrigger;
}
}

AppView<import1.TestFooComponent> viewFactory_TestFooComponent0(AppView<dynamic> parentView, int parentIndex) {
Expand Down
13 changes: 13 additions & 0 deletions _goldens/test/_files/event_tearoff.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import 'package:angular/angular.dart';

@Component(
selector: 'uses-event-tearoff',
template: r'''
<button (click)="onClick" (blur)="onBlur"></button>
''',
)
class UsesEventTearoff {
void onClick(Object e) {}

void onBlur() {}
}
25 changes: 25 additions & 0 deletions _goldens/test/_files/event_tearoff.outline.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// ignore_for_file: library_prefixes,unused_import,no_default_super_constructor_explicit,duplicate_import,unused_shown_name
// The .template.dart files also export the user code.
export 'event_tearoff.dart';

// Required for referencing runtime code.
import 'dart:html';
import 'package:angular/angular.dart';
import 'package:angular/src/core/change_detection/directive_change_detector.dart';
import 'package:angular/src/core/linker/app_view.dart';

// Required for specifically referencing user code.
import 'event_tearoff.dart' as _user;

// Required for "type inference" (scoping).
import 'package:angular/angular.dart';

// For @Component class UsesEventTearoff.
external List<dynamic> get styles$UsesEventTearoff;
external ComponentFactory<_user.UsesEventTearoff> get UsesEventTearoffNgFactory;
external AppView<_user.UsesEventTearoff> viewFactory_UsesEventTearoff0(AppView<dynamic> parentView, int parentIndex);
class ViewUsesEventTearoff0 extends AppView<_user.UsesEventTearoff> {
external ViewUsesEventTearoff0(AppView<dynamic> parentView, int parentIndex);
}

external void initReflector();
94 changes: 94 additions & 0 deletions _goldens/test/_files/event_tearoff.template.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// **************************************************************************
// Generator: Instance of 'Compiler'
// **************************************************************************

// ignore_for_file: cancel_subscriptions,constant_identifier_names,duplicate_import,non_constant_identifier_names,library_prefixes,UNUSED_IMPORT,UNUSED_SHOWN_NAME
import 'event_tearoff.dart';
export 'event_tearoff.dart';
import 'package:angular/angular.dart';
import 'package:angular/src/di/reflector.dart' as _ngRef;
import 'package:angular/angular.template.dart' as _ref0;
import 'package:angular/src/core/linker/app_view.dart';
import 'event_tearoff.dart' as import1;
import 'dart:html' as import2;
import 'package:angular/src/core/render/api.dart';
import 'package:angular/src/core/linker/view_type.dart' as import4;
import 'package:angular/src/core/change_detection/change_detection.dart';
import 'package:angular/src/core/linker/app_view_utils.dart' as import6;
import 'package:angular/src/runtime.dart' as import7;
import 'package:angular/angular.dart';

final List<dynamic> styles$UsesEventTearoff = const [];

class ViewUsesEventTearoff0 extends AppView<import1.UsesEventTearoff> {
import2.ButtonElement _el_0;
static RenderComponentType _renderType;
ViewUsesEventTearoff0(AppView<dynamic> parentView, int parentIndex) : super(import4.ViewType.component, {}, parentView, parentIndex, ChangeDetectionStrategy.CheckAlways) {
rootEl = import2.document.createElement('uses-event-tearoff');
_renderType ??= import6.appViewUtils.createRenderType((import7.isDevMode ? 'asset:_goldens/test/_files/event_tearoff.dart' : null), ViewEncapsulation.None, styles$UsesEventTearoff);
setupComponentType(_renderType);
}
@override
ComponentRef<import1.UsesEventTearoff> build() {
final _rootEl = rootEl;
final import2.HtmlElement parentRenderNode = initViewRoot(_rootEl);
var doc = import2.document;
_el_0 = createAndAppend(doc, 'button', parentRenderNode);
_el_0.addEventListener('click', eventHandler1(ctx.onClick));
_el_0.addEventListener('blur', eventHandler0(ctx.onBlur));
init(const [], null);
return null;
}
}

AppView<import1.UsesEventTearoff> viewFactory_UsesEventTearoff0(AppView<dynamic> parentView, int parentIndex) {
return new ViewUsesEventTearoff0(parentView, parentIndex);
}

final List<dynamic> styles$UsesEventTearoffHost = const [];

class _ViewUsesEventTearoffHost0 extends AppView<import1.UsesEventTearoff> {
ViewUsesEventTearoff0 _compView_0;
import1.UsesEventTearoff _UsesEventTearoff_0_5;
_ViewUsesEventTearoffHost0(AppView<dynamic> parentView, int parentIndex) : super(import4.ViewType.host, {}, parentView, parentIndex, ChangeDetectionStrategy.CheckAlways);
@override
ComponentRef<import1.UsesEventTearoff> build() {
_compView_0 = new ViewUsesEventTearoff0(this, 0);
rootEl = _compView_0.rootEl;
_UsesEventTearoff_0_5 = new import1.UsesEventTearoff();
_compView_0.create(_UsesEventTearoff_0_5, projectableNodes);
init0(rootEl);
return new ComponentRef(0, this, rootEl, _UsesEventTearoff_0_5);
}

@override
void detectChangesInternal() {
_compView_0.detectChanges();
}

@override
void destroyInternal() {
_compView_0?.destroy();
}
}

AppView<import1.UsesEventTearoff> viewFactory_UsesEventTearoffHost0(AppView<dynamic> parentView, int parentIndex) {
return new _ViewUsesEventTearoffHost0(parentView, parentIndex);
}

const ComponentFactory<import1.UsesEventTearoff> _UsesEventTearoffNgFactory = const ComponentFactory('uses-event-tearoff', viewFactory_UsesEventTearoffHost0, _UsesEventTearoffMetadata);
ComponentFactory<import1.UsesEventTearoff> get UsesEventTearoffNgFactory {
return _UsesEventTearoffNgFactory;
}

const _UsesEventTearoffMetadata = const [];
var _visited = false;
void initReflector() {
if (_visited) {
return;
}
_visited = true;

_ngRef.registerComponent(UsesEventTearoff, UsesEventTearoffNgFactory);
_ref0.initReflector();
}
81 changes: 81 additions & 0 deletions _tests/test/core/event_handler_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
@TestOn('browser')
import 'dart:async';
import 'dart:html';

import 'package:angular/angular.dart';
import 'package:angular_test/angular_test.dart';
import 'package:test/test.dart';

import 'event_handler_test.template.dart' as ng;

void main() {
tearDown(disposeAnyRunningTest);

NgTestFixture<ClickHandler> fixture;

group('Event handler', () {
setUp(() async {
final testBed = NgTestBed.forComponent(ng.ClickHandlerNgFactory);
fixture = await testBed.create();
});

group('method call', () {
test('should handle click with no args', () async {
fixture.update((cmp) => cmp.noArgButton.click());
expect(fixture.assertOnlyInstance.clicks, emits(null));
});

test('should handle click with one arg', () async {
fixture.update((cmp) => cmp.oneArgButton.click());
expect(fixture.assertOnlyInstance.clicks, emits(null));
});
});

group('tearoffs', () {
test('should handle click with no args', () async {
fixture.update((cmp) => cmp.noArgTearoffButton.click());
expect(fixture.assertOnlyInstance.clicks, emits(null));
});

test('should handle click with one arg', () async {
fixture.update((cmp) => cmp.oneArgTearoffButton.click());
expect(fixture.assertOnlyInstance.clicks, emits(null));
});
});
});
}

@Component(
selector: 'test',
template: '''
<button #noArg (click)="onClick()"></button>
<button #oneArg (click)="clickWithEvent(\$event)"></button>
<button #noArgTearoff (click)="onClick"></button>
<button #oneArgTearoff (click)="clickWithEvent"></button>
''',
)
class ClickHandler {
@ViewChild('noArg')
HtmlElement noArgButton;

@ViewChild('oneArg')
HtmlElement oneArgButton;

@ViewChild('noArgTearoff')
HtmlElement noArgTearoffButton;

@ViewChild('oneArgTearoff')
HtmlElement oneArgTearoffButton;

void onClick() {
_clicks.add(null);
}

void clickWithEvent(Object event) {
if (event != null) _clicks.add(null);
}

Stream get clicks => _clicks.stream;

final StreamController _clicks = new StreamController();
}
6 changes: 6 additions & 0 deletions angular/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,12 @@ everyone).
<button [attr.disabled.if]="isDisabled"></button>
```
* Add support for tear-offs in event handlers in the templates.
**BEFORE**: `<button (onClick)="clickHandler($event)">`
**AFTER**: `<button (onClick)="clickHandler">`
#### Breaking changes
* We now have a new, more stricter template parser, which strictly requires
Expand Down
32 changes: 32 additions & 0 deletions angular/lib/src/compiler/analyzed_class.dart
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,38 @@ ast.AST rewriteInterpolate(ast.AST original, AnalyzedClass analyzedClass) {
return original;
}

/// Rewrites an event tearoff as a method call.
///
/// If [original] is a [ast.PropertyRead], and a method with the same name
/// exists in [analyzedClass], then convert [original] into a [ast.MethodCall].
///
/// If the underlying method has any parameters, then assume one parameter of
/// '$event'.
ast.AST rewriteTearoff(ast.AST original, AnalyzedClass analyzedClass) {
ast.AST unwrappedExpression = original;
if (original is ast.ASTWithSource) {
unwrappedExpression = original.ast;
}
if (unwrappedExpression is! ast.PropertyRead) return original;
ast.PropertyRead propertyRead = unwrappedExpression;
final method = analyzedClass._classElement.getMethod(propertyRead.name);
if (method == null) return original;

if (method.parameters.isEmpty) {
return _simpleMethodCall(propertyRead);
} else {
return _complexMethodCall(propertyRead);
}
}

ast.AST _simpleMethodCall(ast.PropertyRead propertyRead) =>
new ast.MethodCall(propertyRead.receiver, propertyRead.name, []);

final _eventArg = new ast.PropertyRead(new ast.ImplicitReceiver(), '\$event');

ast.AST _complexMethodCall(ast.PropertyRead propertyRead) =>
new ast.MethodCall(propertyRead.receiver, propertyRead.name, [_eventArg]);

/// Returns [true] if [expression] could be [null].
bool canBeNull(ast.AST expression) {
if (expression is ast.ASTWithSource) {
Expand Down
Loading

0 comments on commit d72bcef

Please sign in to comment.