Skip to content

Commit a7275a2

Browse files
committed
Support component with event handlers
Fixes #42
1 parent 2383f84 commit a7275a2

File tree

5 files changed

+152
-41
lines changed

5 files changed

+152
-41
lines changed

lib/__tests__/__snapshots__/transform.js.snap

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,47 @@ foo
495495
=========="
496496
`;
497497
498+
exports[`native components handles multiple event handlers correctly 1`] = `
499+
"==========
500+
501+
export default class FooComponent extends Component {
502+
mouseDown() {
503+
console.log('Hello!');
504+
}
505+
506+
mouseUp() {
507+
console.log('World!');
508+
}
509+
}
510+
511+
~~~~~~~~~~
512+
foo
513+
~~~~~~~~~~
514+
=> tagName: div
515+
~~~~~~~~~~
516+
517+
import { tagName } from \\"@ember-decorators/component\\";
518+
import { action } from \\"@ember/object\\";
519+
@tagName(\\"\\")
520+
export default class FooComponent extends Component {
521+
@action
522+
handleMouseDown() {
523+
console.log('Hello!');
524+
}
525+
526+
@action
527+
handleMouseUp() {
528+
console.log('World!');
529+
}
530+
}
531+
532+
~~~~~~~~~~
533+
<div ...attributes {{on \\"mouseDown\\" this.handleMouseDown}} {{on \\"mouseUp\\" this.handleMouseUp}}>
534+
foo
535+
</div>
536+
=========="
537+
`;
538+
498539
exports[`native components handles single \`@classNames\` item correctly 1`] = `
499540
"==========
500541
@@ -523,6 +564,38 @@ foo
523564
=========="
524565
`;
525566
567+
exports[`native components handles single event handler correctly 1`] = `
568+
"==========
569+
570+
export default class FooComponent extends Component {
571+
click() {
572+
console.log('Hello World!');
573+
}
574+
}
575+
576+
~~~~~~~~~~
577+
foo
578+
~~~~~~~~~~
579+
=> tagName: div
580+
~~~~~~~~~~
581+
582+
import { tagName } from \\"@ember-decorators/component\\";
583+
import { action } from \\"@ember/object\\";
584+
@tagName(\\"\\")
585+
export default class FooComponent extends Component {
586+
@action
587+
handleClick() {
588+
console.log('Hello World!');
589+
}
590+
}
591+
592+
~~~~~~~~~~
593+
<div ...attributes {{on \\"click\\" this.handleClick}}>
594+
foo
595+
</div>
596+
=========="
597+
`;
598+
526599
exports[`native components multi-line template 1`] = `
527600
"==========
528601
export default class extends Component {};

lib/__tests__/transform.js

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,38 @@ describe('native components', function() {
399399
expect(generateSnapshot(source, template)).toMatchSnapshot();
400400
});
401401

402+
test('handles single event handler correctly', () => {
403+
let source = `
404+
export default class FooComponent extends Component {
405+
click() {
406+
console.log('Hello World!');
407+
}
408+
}
409+
`;
410+
411+
let template = `foo`;
412+
413+
expect(generateSnapshot(source, template)).toMatchSnapshot();
414+
});
415+
416+
test('handles multiple event handlers correctly', () => {
417+
let source = `
418+
export default class FooComponent extends Component {
419+
mouseDown() {
420+
console.log('Hello!');
421+
}
422+
423+
mouseUp() {
424+
console.log('World!');
425+
}
426+
}
427+
`;
428+
429+
let template = `foo`;
430+
431+
expect(generateSnapshot(source, template)).toMatchSnapshot();
432+
});
433+
402434
test('throws for non-boolean @classNameBindings', () => {
403435
let source = `
404436
import { classNameBindings } from '@ember-decorators/component';
@@ -458,34 +490,6 @@ describe('native components', function() {
458490
);
459491
});
460492

461-
test('throws if component is using `keyDown()`', () => {
462-
let source = `
463-
export default class FooComponent extends Component {
464-
keyDown() {
465-
console.log('Hello World!');
466-
}
467-
}
468-
`;
469-
470-
expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot(
471-
`"Using \`keyDown()\` is not supported in tagless components"`
472-
);
473-
});
474-
475-
test('throws if component is using `click()`', () => {
476-
let source = `
477-
export default class FooComponent extends Component {
478-
click() {
479-
console.log('Hello World!');
480-
}
481-
}
482-
`;
483-
484-
expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot(
485-
`"Using \`click()\` is not supported in tagless components"`
486-
);
487-
});
488-
489493
test('multi-line template', () => {
490494
let source = `export default class extends Component {};`;
491495

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, 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

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)