From 03e8e80e8fca8be676bb21ae802ea907adf24c71 Mon Sep 17 00:00:00 2001 From: simonihmig Date: Thu, 16 Jan 2020 23:16:59 +0100 Subject: [PATCH] Support component with event handlers Fixes #42 --- lib/__tests__/__snapshots__/transform.js.snap | 81 +++++++++++++++++++ lib/__tests__/transform.js | 68 ++++++++-------- lib/transform/native.js | 32 +++++--- lib/transform/template.js | 10 ++- lib/utils/native.js | 18 ++++- 5 files changed, 164 insertions(+), 45 deletions(-) diff --git a/lib/__tests__/__snapshots__/transform.js.snap b/lib/__tests__/__snapshots__/transform.js.snap index 6932bdd..c227d2c 100644 --- a/lib/__tests__/__snapshots__/transform.js.snap +++ b/lib/__tests__/__snapshots__/transform.js.snap @@ -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!'); + } + } + +~~~~~~~~~~ +
+ foo +
+==========" +`; + exports[`native components handles single \`@classNames\` item correctly 1`] = ` "========== @@ -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!'); + } + } + +~~~~~~~~~~ +
+ foo +
+==========" +`; + exports[`native components keeps unrelated decorators in place 1`] = ` "========== diff --git a/lib/__tests__/transform.js b/lib/__tests__/transform.js index a7e89ba..3cb8658 100644 --- a/lib/__tests__/transform.js +++ b/lib/__tests__/transform.js @@ -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'; @@ -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'; diff --git a/lib/transform/native.js b/lib/transform/native.js index 566dbf1..c6c71de 100644 --- a/lib/transform/native.js +++ b/lib/transform/native.js @@ -13,6 +13,7 @@ const { removeDecorator, ensureImport, isProperty, + renameEventHandler, } = require('../utils/native'); const EVENT_HANDLER_METHODS = [ @@ -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); @@ -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'); @@ -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, + }; }; diff --git a/lib/transform/template.js b/lib/transform/template.js index fc0171e..6561bec 100644 --- a/lib/transform/template.js +++ b/lib/transform/template.js @@ -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 @@ -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`)], }), ]; diff --git a/lib/utils/native.js b/lib/utils/native.js index ff3e486..b5329a1 100644 --- a/lib/utils/native.js +++ b/lib/utils/native.js @@ -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)) ); } } @@ -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; } @@ -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, @@ -270,4 +281,5 @@ module.exports = { removeDecorator, ensureImport, removeImport, + renameEventHandler, };