Skip to content

Commit 188ca76

Browse files
committed
Support component with event handlers
Fixes #42
1 parent 45cfeca commit 188ca76

File tree

6 files changed

+154
-40
lines changed

6 files changed

+154
-40
lines changed

lib/__tests__/__snapshots__/transform.js.snap

+73
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,47 @@ foo
471471
=========="
472472
`;
473473
474+
exports[`native components handles multiple event handlers correctly 1`] = `
475+
"==========
476+
477+
export default class FooComponent extends Component {
478+
mouseDown() {
479+
console.log('Hello!');
480+
}
481+
482+
mouseUp() {
483+
console.log('World!');
484+
}
485+
}
486+
487+
~~~~~~~~~~
488+
foo
489+
~~~~~~~~~~
490+
=> tagName: div
491+
~~~~~~~~~~
492+
493+
import { tagName } from \\"@ember-decorators/component\\";
494+
import { action } from \\"@ember/object\\";
495+
@tagName(\\"\\")
496+
export default class FooComponent extends Component {
497+
@action
498+
handleMouseDown() {
499+
console.log('Hello!');
500+
}
501+
502+
@action
503+
handleMouseUp() {
504+
console.log('World!');
505+
}
506+
}
507+
508+
~~~~~~~~~~
509+
<div ...attributes {{on \\"mouseDown\\" this.handleMouseDown}} {{on \\"mouseUp\\" this.handleMouseUp}}>
510+
foo
511+
</div>
512+
=========="
513+
`;
514+
474515
exports[`native components handles single \`@classNames\` item correctly 1`] = `
475516
"==========
476517
@@ -499,6 +540,38 @@ foo
499540
=========="
500541
`;
501542
543+
exports[`native components handles single event handler correctly 1`] = `
544+
"==========
545+
546+
export default class FooComponent extends Component {
547+
click() {
548+
console.log('Hello World!');
549+
}
550+
}
551+
552+
~~~~~~~~~~
553+
foo
554+
~~~~~~~~~~
555+
=> tagName: div
556+
~~~~~~~~~~
557+
558+
import { tagName } from \\"@ember-decorators/component\\";
559+
import { action } from \\"@ember/object\\";
560+
@tagName(\\"\\")
561+
export default class FooComponent extends Component {
562+
@action
563+
handleClick() {
564+
console.log('Hello World!');
565+
}
566+
}
567+
568+
~~~~~~~~~~
569+
<div ...attributes {{on \\"click\\" this.handleClick}}>
570+
foo
571+
</div>
572+
=========="
573+
`;
574+
502575
exports[`native components multi-line template 1`] = `
503576
"==========
504577
export default class extends Component {};

lib/__tests__/transform.js

+32-28
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,38 @@ describe('native components', function() {
373373
expect(generateSnapshot(source, template)).toMatchSnapshot();
374374
});
375375

376+
test('handles single event handler correctly', () => {
377+
let source = `
378+
export default class FooComponent extends Component {
379+
click() {
380+
console.log('Hello World!');
381+
}
382+
}
383+
`;
384+
385+
let template = `foo`;
386+
387+
expect(generateSnapshot(source, template)).toMatchSnapshot();
388+
});
389+
390+
test('handles multiple event handlers correctly', () => {
391+
let source = `
392+
export default class FooComponent extends Component {
393+
mouseDown() {
394+
console.log('Hello!');
395+
}
396+
397+
mouseUp() {
398+
console.log('World!');
399+
}
400+
}
401+
`;
402+
403+
let template = `foo`;
404+
405+
expect(generateSnapshot(source, template)).toMatchSnapshot();
406+
});
407+
376408
test('throws for non-boolean @classNameBindings', () => {
377409
let source = `
378410
import { classNameBindings } from '@ember-decorators/component';
@@ -432,34 +464,6 @@ describe('native components', function() {
432464
);
433465
});
434466

435-
test('throws if component is using `keyDown()`', () => {
436-
let source = `
437-
export default class FooComponent extends Component {
438-
keyDown() {
439-
console.log('Hello World!');
440-
}
441-
}
442-
`;
443-
444-
expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot(
445-
`"Using \`keyDown()\` is not supported in tagless components"`
446-
);
447-
});
448-
449-
test('throws if component is using `click()`', () => {
450-
let source = `
451-
export default class FooComponent extends Component {
452-
click() {
453-
console.log('Hello World!');
454-
}
455-
}
456-
`;
457-
458-
expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot(
459-
`"Using \`click()\` is not supported in tagless components"`
460-
);
461-
});
462-
463467
test('multi-line template', () => {
464468
let source = `export default class extends Component {};`;
465469

lib/transform.js

+2
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ function transform(source, template, options = {}) {
8484
classNames,
8585
classNameBindings,
8686
attributeBindings,
87+
eventHandlers,
8788
} = result;
8889
let newTemplate = transformTemplate(
8990
template,
@@ -92,6 +93,7 @@ function transform(source, template, options = {}) {
9293
classNames,
9394
classNameBindings,
9495
attributeBindings,
96+
eventHandlers,
9597
options
9698
);
9799

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, 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
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module.exports = function transformTemplate(
1313
classNames,
1414
classNameBindings,
1515
attributeBindings,
16+
eventHandlers,
1617
options
1718
) {
1819
// wrap existing template with root element
@@ -51,11 +52,19 @@ module.exports = function transformTemplate(
5152
}
5253
attrs.push(b.attr('...attributes', b.text('')));
5354

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

5664
templateAST.body = [
5765
b.element(tagName, {
5866
attrs,
67+
modifiers,
5968
children: [b.text(`\n${PLACEHOLDER}\n`)],
6069
}),
6170
];

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)