Skip to content

Commit

Permalink
Support component with event handlers
Browse files Browse the repository at this point in the history
Fixes #42
  • Loading branch information
simonihmig committed Feb 25, 2020
1 parent c959410 commit 03e8e80
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 45 deletions.
81 changes: 81 additions & 0 deletions lib/__tests__/__snapshots__/transform.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,51 @@ foo
=========="
`;
exports[`native components handles multiple event handlers correctly 1`] = `
"==========
import Component from '@ember/component';
export default class FooComponent extends Component {
mouseDown() {
console.log('Hello!');
}
mouseUp() {
console.log('World!');
}
}
~~~~~~~~~~
foo
~~~~~~~~~~
=> tagName: div
~~~~~~~~~~
import { tagName } from \\"@ember-decorators/component\\";
import { action } from \\"@ember/object\\";
import Component from '@ember/component';
@tagName(\\"\\")
export default class FooComponent extends Component {
@action
handleMouseDown() {
console.log('Hello!');
}
@action
handleMouseUp() {
console.log('World!');
}
}
~~~~~~~~~~
<div ...attributes {{on \\"mousedown\\" this.handleMouseDown}} {{on \\"mouseup\\" this.handleMouseUp}}>
foo
</div>
=========="
`;

exports[`native components handles single \`@classNames\` item correctly 1`] = `
"==========
Expand Down Expand Up @@ -589,6 +634,42 @@ foo
=========="
`;
exports[`native components handles single event handler correctly 1`] = `
"==========
import Component from '@ember/component';
export default class FooComponent extends Component {
click() {
console.log('Hello World!');
}
}
~~~~~~~~~~
foo
~~~~~~~~~~
=> tagName: div
~~~~~~~~~~
import { tagName } from \\"@ember-decorators/component\\";
import { action } from \\"@ember/object\\";
import Component from '@ember/component';
@tagName(\\"\\")
export default class FooComponent extends Component {
@action
handleClick() {
console.log('Hello World!');
}
}
~~~~~~~~~~
<div ...attributes {{on \\"click\\" this.handleClick}}>
foo
</div>
=========="
`;

exports[`native components keeps unrelated decorators in place 1`] = `
"==========
Expand Down
68 changes: 36 additions & 32 deletions lib/__tests__/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,42 @@ describe('native components', function() {
expect(generateSnapshot(source, template)).toMatchSnapshot();
});

test('handles single event handler correctly', () => {
let source = `
import Component from '@ember/component';
export default class FooComponent extends Component {
click() {
console.log('Hello World!');
}
}
`;

let template = `foo`;

expect(generateSnapshot(source, template)).toMatchSnapshot();
});

test('handles multiple event handlers correctly', () => {
let source = `
import Component from '@ember/component';
export default class FooComponent extends Component {
mouseDown() {
console.log('Hello!');
}
mouseUp() {
console.log('World!');
}
}
`;

let template = `foo`;

expect(generateSnapshot(source, template)).toMatchSnapshot();
});

test('throws for non-boolean @classNameBindings', () => {
let source = `
import Component from '@ember/component';
Expand Down Expand Up @@ -538,38 +574,6 @@ describe('native components', function() {
);
});

test('throws if component is using `keyDown()`', () => {
let source = `
import Component from '@ember/component';
export default class FooComponent extends Component {
keyDown() {
console.log('Hello World!');
}
}
`;

expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot(
`"Using \`keyDown()\` is not supported in tagless components"`
);
});

test('throws if component is using `click()`', () => {
let source = `
import Component from '@ember/component';
export default class FooComponent extends Component {
click() {
console.log('Hello World!');
}
}
`;

expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot(
`"Using \`click()\` is not supported in tagless components"`
);
});

test('multi-line template', () => {
let source = `
import Component from '@ember/component';
Expand Down
32 changes: 23 additions & 9 deletions lib/transform/native.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const {
removeDecorator,
ensureImport,
isProperty,
renameEventHandler,
} = require('../utils/native');

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

// skip components that use `click()` etc.
for (let methodName of EVENT_HANDLER_METHODS) {
let handlerMethod = classBody.filter(path => isMethod(path, methodName))[0];
if (handlerMethod) {
throw new SilentError(`Using \`${methodName}()\` is not supported in tagless components`);
}
}

// analyze `elementId`, `attributeBindings`, `classNames` and `classNameBindings`
let elementId = findElementId(classBody);
debug('elementId: %o', elementId);
Expand All @@ -119,6 +112,19 @@ module.exports = function transformNativeComponent(root, options) {
let classNameBindings = findClassNameBindings(classDeclaration);
debug('classNameBindings: %o', classNameBindings);

let eventHandlers = new Map();
// rename event handlers and add @action
for (let eventName of EVENT_HANDLER_METHODS) {
let handlerMethod = classBody.filter(path => isMethod(path, eventName))[0];

if (handlerMethod) {
let methodName = renameEventHandler(handlerMethod);
addClassDecorator(handlerMethod, 'action');
ensureImport(root, 'action', '@ember/object');
eventHandlers.set(eventName.toLowerCase(), methodName);
}
}

// set `@tagName('')`
addClassDecorator(classDeclaration, 'tagName', [j.stringLiteral('')]);
ensureImport(root, 'tagName', '@ember-decorators/component');
Expand All @@ -142,5 +148,13 @@ module.exports = function transformNativeComponent(root, options) {

let newSource = root.toSource();

return { newSource, tagName, elementId, classNames, classNameBindings, attributeBindings };
return {
newSource,
tagName,
elementId,
classNames,
classNameBindings,
attributeBindings,
eventHandlers,
};
};
10 changes: 9 additions & 1 deletion lib/transform/template.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const PLACEHOLDER = '@@@PLACEHOLDER@@@';

module.exports = function transformTemplate(
template,
{ tagName, elementId, classNames, classNameBindings, attributeBindings, ariaRole },
{ tagName, elementId, classNames, classNameBindings, attributeBindings, ariaRole, eventHandlers },
options
) {
// wrap existing template with root element
Expand Down Expand Up @@ -50,11 +50,19 @@ module.exports = function transformTemplate(
}
attrs.push(b.attr('...attributes', b.text('')));

let modifiers = [];
if (eventHandlers) {
eventHandlers.forEach((methodName, eventName) => {
modifiers.push(b.elementModifier('on', [b.string(eventName), b.path(`this.${methodName}`)]));
});
}

let templateAST = templateRecast.parse(template);

templateAST.body = [
b.element(tagName, {
attrs,
modifiers,
children: [b.text(`\n${PLACEHOLDER}\n`)],
}),
];
Expand Down
18 changes: 15 additions & 3 deletions lib/utils/native.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ function addClassDecorator(classDeclaration, name, args) {
if (existing) {
existing.value.expression.arguments = args;
} else {
if (classDeclaration.value.decorators === undefined) {
if (!classDeclaration.value.decorators) {
classDeclaration.value.decorators = [];
}
classDeclaration.value.decorators.unshift(
j.decorator(j.callExpression(j.identifier(name), args))
args === undefined
? j.decorator(j.identifier(name))
: j.decorator(j.callExpression(j.identifier(name), args))
);
}
}
Expand Down Expand Up @@ -56,7 +58,7 @@ function findStringProperty(properties, name, defaultValue = null) {

function findDecorator(path, name, withArgs) {
let decorators = path.get('decorators');
if (decorators.value === undefined) {
if (!decorators.value) {
return;
}

Expand Down Expand Up @@ -257,6 +259,15 @@ function createImportStatement(source, imported, local) {
return declaration;
}

function renameEventHandler(path) {
let oldName = path.value.key.name;
let newName = `handle${oldName.charAt(0).toUpperCase()}${oldName.slice(1)}`;

path.value.key.name = newName;

return newName;
}

module.exports = {
addClassDecorator,
isProperty,
Expand All @@ -270,4 +281,5 @@ module.exports = {
removeDecorator,
ensureImport,
removeImport,
renameEventHandler,
};

0 comments on commit 03e8e80

Please sign in to comment.