Skip to content

Commit 03e8e80

Browse files
committed
Support component with event handlers
Fixes #42
1 parent c959410 commit 03e8e80

File tree

5 files changed

+164
-45
lines changed

5 files changed

+164
-45
lines changed

lib/__tests__/__snapshots__/transform.js.snap

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,51 @@ foo
559559
=========="
560560
`;
561561
562+
exports[`native components handles multiple event handlers correctly 1`] = `
563+
"==========
564+
565+
import Component from '@ember/component';
566+
567+
export default class FooComponent extends Component {
568+
mouseDown() {
569+
console.log('Hello!');
570+
}
571+
572+
mouseUp() {
573+
console.log('World!');
574+
}
575+
}
576+
577+
~~~~~~~~~~
578+
foo
579+
~~~~~~~~~~
580+
=> tagName: div
581+
~~~~~~~~~~
582+
583+
import { tagName } from \\"@ember-decorators/component\\";
584+
import { action } from \\"@ember/object\\";
585+
import Component from '@ember/component';
586+
587+
@tagName(\\"\\")
588+
export default class FooComponent extends Component {
589+
@action
590+
handleMouseDown() {
591+
console.log('Hello!');
592+
}
593+
594+
@action
595+
handleMouseUp() {
596+
console.log('World!');
597+
}
598+
}
599+
600+
~~~~~~~~~~
601+
<div ...attributes {{on \\"mousedown\\" this.handleMouseDown}} {{on \\"mouseup\\" this.handleMouseUp}}>
602+
foo
603+
</div>
604+
=========="
605+
`;
606+
562607
exports[`native components handles single \`@classNames\` item correctly 1`] = `
563608
"==========
564609
@@ -589,6 +634,42 @@ foo
589634
=========="
590635
`;
591636
637+
exports[`native components handles single event handler correctly 1`] = `
638+
"==========
639+
640+
import Component from '@ember/component';
641+
642+
export default class FooComponent extends Component {
643+
click() {
644+
console.log('Hello World!');
645+
}
646+
}
647+
648+
~~~~~~~~~~
649+
foo
650+
~~~~~~~~~~
651+
=> tagName: div
652+
~~~~~~~~~~
653+
654+
import { tagName } from \\"@ember-decorators/component\\";
655+
import { action } from \\"@ember/object\\";
656+
import Component from '@ember/component';
657+
658+
@tagName(\\"\\")
659+
export default class FooComponent extends Component {
660+
@action
661+
handleClick() {
662+
console.log('Hello World!');
663+
}
664+
}
665+
666+
~~~~~~~~~~
667+
<div ...attributes {{on \\"click\\" this.handleClick}}>
668+
foo
669+
</div>
670+
=========="
671+
`;
672+
592673
exports[`native components keeps unrelated decorators in place 1`] = `
593674
"==========
594675

lib/__tests__/transform.js

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,42 @@ describe('native components', function() {
457457
expect(generateSnapshot(source, template)).toMatchSnapshot();
458458
});
459459

460+
test('handles single event handler correctly', () => {
461+
let source = `
462+
import Component from '@ember/component';
463+
464+
export default class FooComponent extends Component {
465+
click() {
466+
console.log('Hello World!');
467+
}
468+
}
469+
`;
470+
471+
let template = `foo`;
472+
473+
expect(generateSnapshot(source, template)).toMatchSnapshot();
474+
});
475+
476+
test('handles multiple event handlers correctly', () => {
477+
let source = `
478+
import Component from '@ember/component';
479+
480+
export default class FooComponent extends Component {
481+
mouseDown() {
482+
console.log('Hello!');
483+
}
484+
485+
mouseUp() {
486+
console.log('World!');
487+
}
488+
}
489+
`;
490+
491+
let template = `foo`;
492+
493+
expect(generateSnapshot(source, template)).toMatchSnapshot();
494+
});
495+
460496
test('throws for non-boolean @classNameBindings', () => {
461497
let source = `
462498
import Component from '@ember/component';
@@ -538,38 +574,6 @@ describe('native components', function() {
538574
);
539575
});
540576

541-
test('throws if component is using `keyDown()`', () => {
542-
let source = `
543-
import Component from '@ember/component';
544-
545-
export default class FooComponent extends Component {
546-
keyDown() {
547-
console.log('Hello World!');
548-
}
549-
}
550-
`;
551-
552-
expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot(
553-
`"Using \`keyDown()\` is not supported in tagless components"`
554-
);
555-
});
556-
557-
test('throws if component is using `click()`', () => {
558-
let source = `
559-
import Component from '@ember/component';
560-
561-
export default class FooComponent extends Component {
562-
click() {
563-
console.log('Hello World!');
564-
}
565-
}
566-
`;
567-
568-
expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot(
569-
`"Using \`click()\` is not supported in tagless components"`
570-
);
571-
});
572-
573577
test('multi-line template', () => {
574578
let source = `
575579
import Component from '@ember/component';

lib/transform/native.js

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const {
1313
removeDecorator,
1414
ensureImport,
1515
isProperty,
16+
renameEventHandler,
1617
} = require('../utils/native');
1718

1819
const EVENT_HANDLER_METHODS = [
@@ -98,14 +99,6 @@ module.exports = function transformNativeComponent(root, options) {
9899
throw new SilentError(`Using \`this.elementId\` is not supported in tagless components`);
99100
}
100101

101-
// skip components that use `click()` etc.
102-
for (let methodName of EVENT_HANDLER_METHODS) {
103-
let handlerMethod = classBody.filter(path => isMethod(path, methodName))[0];
104-
if (handlerMethod) {
105-
throw new SilentError(`Using \`${methodName}()\` is not supported in tagless components`);
106-
}
107-
}
108-
109102
// analyze `elementId`, `attributeBindings`, `classNames` and `classNameBindings`
110103
let elementId = findElementId(classBody);
111104
debug('elementId: %o', elementId);
@@ -119,6 +112,19 @@ module.exports = function transformNativeComponent(root, options) {
119112
let classNameBindings = findClassNameBindings(classDeclaration);
120113
debug('classNameBindings: %o', classNameBindings);
121114

115+
let eventHandlers = new Map();
116+
// rename event handlers and add @action
117+
for (let eventName of EVENT_HANDLER_METHODS) {
118+
let handlerMethod = classBody.filter(path => isMethod(path, eventName))[0];
119+
120+
if (handlerMethod) {
121+
let methodName = renameEventHandler(handlerMethod);
122+
addClassDecorator(handlerMethod, 'action');
123+
ensureImport(root, 'action', '@ember/object');
124+
eventHandlers.set(eventName.toLowerCase(), methodName);
125+
}
126+
}
127+
122128
// set `@tagName('')`
123129
addClassDecorator(classDeclaration, 'tagName', [j.stringLiteral('')]);
124130
ensureImport(root, 'tagName', '@ember-decorators/component');
@@ -142,5 +148,13 @@ module.exports = function transformNativeComponent(root, options) {
142148

143149
let newSource = root.toSource();
144150

145-
return { newSource, tagName, elementId, classNames, classNameBindings, attributeBindings };
151+
return {
152+
newSource,
153+
tagName,
154+
elementId,
155+
classNames,
156+
classNameBindings,
157+
attributeBindings,
158+
eventHandlers,
159+
};
146160
};

lib/transform/template.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const PLACEHOLDER = '@@@PLACEHOLDER@@@';
88

99
module.exports = function transformTemplate(
1010
template,
11-
{ tagName, elementId, classNames, classNameBindings, attributeBindings, ariaRole },
11+
{ tagName, elementId, classNames, classNameBindings, attributeBindings, ariaRole, eventHandlers },
1212
options
1313
) {
1414
// wrap existing template with root element
@@ -50,11 +50,19 @@ module.exports = function transformTemplate(
5050
}
5151
attrs.push(b.attr('...attributes', b.text('')));
5252

53+
let modifiers = [];
54+
if (eventHandlers) {
55+
eventHandlers.forEach((methodName, eventName) => {
56+
modifiers.push(b.elementModifier('on', [b.string(eventName), b.path(`this.${methodName}`)]));
57+
});
58+
}
59+
5360
let templateAST = templateRecast.parse(template);
5461

5562
templateAST.body = [
5663
b.element(tagName, {
5764
attrs,
65+
modifiers,
5866
children: [b.text(`\n${PLACEHOLDER}\n`)],
5967
}),
6068
];

lib/utils/native.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ function addClassDecorator(classDeclaration, name, args) {
88
if (existing) {
99
existing.value.expression.arguments = args;
1010
} else {
11-
if (classDeclaration.value.decorators === undefined) {
11+
if (!classDeclaration.value.decorators) {
1212
classDeclaration.value.decorators = [];
1313
}
1414
classDeclaration.value.decorators.unshift(
15-
j.decorator(j.callExpression(j.identifier(name), args))
15+
args === undefined
16+
? j.decorator(j.identifier(name))
17+
: j.decorator(j.callExpression(j.identifier(name), args))
1618
);
1719
}
1820
}
@@ -56,7 +58,7 @@ function findStringProperty(properties, name, defaultValue = null) {
5658

5759
function findDecorator(path, name, withArgs) {
5860
let decorators = path.get('decorators');
59-
if (decorators.value === undefined) {
61+
if (!decorators.value) {
6062
return;
6163
}
6264

@@ -257,6 +259,15 @@ function createImportStatement(source, imported, local) {
257259
return declaration;
258260
}
259261

262+
function renameEventHandler(path) {
263+
let oldName = path.value.key.name;
264+
let newName = `handle${oldName.charAt(0).toUpperCase()}${oldName.slice(1)}`;
265+
266+
path.value.key.name = newName;
267+
268+
return newName;
269+
}
270+
260271
module.exports = {
261272
addClassDecorator,
262273
isProperty,
@@ -270,4 +281,5 @@ module.exports = {
270281
removeDecorator,
271282
ensureImport,
272283
removeImport,
284+
renameEventHandler,
273285
};

0 commit comments

Comments
 (0)