Skip to content

Commit 263b2a3

Browse files
committed
Support component with event handlers
Fixes #42
1 parent 11ce8b5 commit 263b2a3

File tree

5 files changed

+164
-45
lines changed

5 files changed

+164
-45
lines changed

lib/__tests__/__snapshots__/transform.js.snap

+81
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,51 @@ foo
517517
=========="
518518
`;
519519
520+
exports[`native components handles multiple event handlers correctly 1`] = `
521+
"==========
522+
523+
import Component from '@ember/component';
524+
525+
export default class FooComponent extends Component {
526+
mouseDown() {
527+
console.log('Hello!');
528+
}
529+
530+
mouseUp() {
531+
console.log('World!');
532+
}
533+
}
534+
535+
~~~~~~~~~~
536+
foo
537+
~~~~~~~~~~
538+
=> tagName: div
539+
~~~~~~~~~~
540+
541+
import { tagName } from \\"@ember-decorators/component\\";
542+
import { action } from \\"@ember/object\\";
543+
import Component from '@ember/component';
544+
545+
@tagName(\\"\\")
546+
export default class FooComponent extends Component {
547+
@action
548+
handleMouseDown() {
549+
console.log('Hello!');
550+
}
551+
552+
@action
553+
handleMouseUp() {
554+
console.log('World!');
555+
}
556+
}
557+
558+
~~~~~~~~~~
559+
<div ...attributes {{on \\"mousedown\\" this.handleMouseDown}} {{on \\"mouseup\\" this.handleMouseUp}}>
560+
foo
561+
</div>
562+
=========="
563+
`;
564+
520565
exports[`native components handles single \`@classNames\` item correctly 1`] = `
521566
"==========
522567
@@ -547,6 +592,42 @@ foo
547592
=========="
548593
`;
549594
595+
exports[`native components handles single event handler correctly 1`] = `
596+
"==========
597+
598+
import Component from '@ember/component';
599+
600+
export default class FooComponent extends Component {
601+
click() {
602+
console.log('Hello World!');
603+
}
604+
}
605+
606+
~~~~~~~~~~
607+
foo
608+
~~~~~~~~~~
609+
=> tagName: div
610+
~~~~~~~~~~
611+
612+
import { tagName } from \\"@ember-decorators/component\\";
613+
import { action } from \\"@ember/object\\";
614+
import Component from '@ember/component';
615+
616+
@tagName(\\"\\")
617+
export default class FooComponent extends Component {
618+
@action
619+
handleClick() {
620+
console.log('Hello World!');
621+
}
622+
}
623+
624+
~~~~~~~~~~
625+
<div ...attributes {{on \\"click\\" this.handleClick}}>
626+
foo
627+
</div>
628+
=========="
629+
`;
630+
550631
exports[`native components multi-line template 1`] = `
551632
"==========
552633

lib/__tests__/transform.js

+36-32
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,42 @@ describe('native components', function() {
437437
expect(generateSnapshot(source, template)).toMatchSnapshot();
438438
});
439439

440+
test('handles single event handler correctly', () => {
441+
let source = `
442+
import Component from '@ember/component';
443+
444+
export default class FooComponent extends Component {
445+
click() {
446+
console.log('Hello World!');
447+
}
448+
}
449+
`;
450+
451+
let template = `foo`;
452+
453+
expect(generateSnapshot(source, template)).toMatchSnapshot();
454+
});
455+
456+
test('handles multiple event handlers correctly', () => {
457+
let source = `
458+
import Component from '@ember/component';
459+
460+
export default class FooComponent extends Component {
461+
mouseDown() {
462+
console.log('Hello!');
463+
}
464+
465+
mouseUp() {
466+
console.log('World!');
467+
}
468+
}
469+
`;
470+
471+
let template = `foo`;
472+
473+
expect(generateSnapshot(source, template)).toMatchSnapshot();
474+
});
475+
440476
test('throws for non-boolean @classNameBindings', () => {
441477
let source = `
442478
import Component from '@ember/component';
@@ -502,38 +538,6 @@ describe('native components', function() {
502538
);
503539
});
504540

505-
test('throws if component is using `keyDown()`', () => {
506-
let source = `
507-
import Component from '@ember/component';
508-
509-
export default class FooComponent extends Component {
510-
keyDown() {
511-
console.log('Hello World!');
512-
}
513-
}
514-
`;
515-
516-
expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot(
517-
`"Using \`keyDown()\` is not supported in tagless components"`
518-
);
519-
});
520-
521-
test('throws if component is using `click()`', () => {
522-
let source = `
523-
import Component from '@ember/component';
524-
525-
export default class FooComponent extends Component {
526-
click() {
527-
console.log('Hello World!');
528-
}
529-
}
530-
`;
531-
532-
expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot(
533-
`"Using \`click()\` is not supported in tagless components"`
534-
);
535-
});
536-
537541
test('multi-line template', () => {
538542
let source = `
539543
import Component from '@ember/component';

lib/transform/native.js

+23-9
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(exportDefaultDeclaration, '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

+9-1
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

+15-3
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)