From 89eff29101550978faa2f2934b84514ae8cb907b Mon Sep 17 00:00:00 2001 From: simonihmig Date: Fri, 12 Jul 2019 11:37:53 +0200 Subject: [PATCH] Add polyfill for angle brackets Partially implements #66 --- .eslintrc.js | 6 +- addon/helpers/-link-to-params.js | 28 ++++ app/helpers/-link-to-params.js | 1 + index.js | 33 +++++ lib/ast-link-to-transform.js | 133 +++++++++++++++++++ tests/integration/components/link-to-test.js | 109 +++++++++++++++ 6 files changed, 307 insertions(+), 3 deletions(-) create mode 100644 addon/helpers/-link-to-params.js create mode 100644 app/helpers/-link-to-params.js create mode 100644 lib/ast-link-to-transform.js create mode 100644 tests/integration/components/link-to-test.js diff --git a/.eslintrc.js b/.eslintrc.js index ef236c6..cbdf8a7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,18 +6,18 @@ module.exports = { }, plugins: [ 'ember', - 'prettier', + // 'prettier', ], extends: [ 'eslint:recommended', 'plugin:ember/recommended', - 'prettier', + // 'prettier', ], env: { browser: true }, rules: { - 'prettier/prettier': 'error', + // 'prettier/prettier': 'error', }, overrides: [ // node files diff --git a/addon/helpers/-link-to-params.js b/addon/helpers/-link-to-params.js new file mode 100644 index 0000000..6068647 --- /dev/null +++ b/addon/helpers/-link-to-params.js @@ -0,0 +1,28 @@ +import { helper } from '@ember/component/helper'; + +function linkToParams(_params, { route, model, models, query }) { + let params = []; + + if (route) { + params.push(route); + } + + if (model) { + params.push(model); + } + + if (models) { + params.push(...models); + } + + if (query) { + params.push({ + isQueryParams: true, + values: query + }); + } + + return params; +} + +export default helper(linkToParams); diff --git a/app/helpers/-link-to-params.js b/app/helpers/-link-to-params.js new file mode 100644 index 0000000..811df3c --- /dev/null +++ b/app/helpers/-link-to-params.js @@ -0,0 +1 @@ +export { default } from 'ember-angle-bracket-invocation-polyfill/helpers/-link-to-params'; diff --git a/index.js b/index.js index e2be415..d70653a 100644 --- a/index.js +++ b/index.js @@ -13,6 +13,7 @@ module.exports = { this.shouldPolyfill = emberVersion.lt('3.4.0-alpha.1'); this.shouldPolyfillNested = emberVersion.lt('3.10.0-alpha.1'); + this.shouldPolyfillBuiltinComponents = emberVersion.lt('3.10.0-alpha.1'); let parentChecker = new VersionChecker(this.parent); let precompileVersion = parentChecker.for('ember-cli-htmlbars-inline-precompile'); @@ -43,6 +44,16 @@ module.exports = { }; registry.add('htmlbars-ast-plugin', pluginObj); } + + if (this.shouldPolyfillBuiltinComponents) { + let pluginObj = this._buildLinkToPlugin(); + pluginObj.parallelBabel = { + requireFile: __filename, + buildUsing: '_buildLinkToPlugin', + params: {}, + }; + registry.add('htmlbars-ast-plugin', pluginObj); + } }, _buildPlugin() { @@ -65,6 +76,16 @@ module.exports = { }; }, + _buildLinkToPlugin() { + return { + name: 'link-to-component-invocation-support', + plugin: require('./lib/ast-link-to-transform'), + baseDir() { + return __dirname; + }, + }; + }, + included() { this._super.included.apply(this, arguments); @@ -92,4 +113,16 @@ module.exports = { return transpiledVendorTree; }, + + treeForAddon() { + if (this.shouldPolyfillBuiltinComponents) { + return this._super.treeForAddon.apply(this, arguments); + } + }, + + treeForApp() { + if (this.shouldPolyfillBuiltinComponents) { + return this._super.treeForApp.apply(this, arguments); + } + }, }; diff --git a/lib/ast-link-to-transform.js b/lib/ast-link-to-transform.js new file mode 100644 index 0000000..17c1903 --- /dev/null +++ b/lib/ast-link-to-transform.js @@ -0,0 +1,133 @@ +'use strict'; + +const reservedProps = [ + '@route', + '@model', + '@models', + '@query', +]; +const supportedHTMLAttributes = [ + 'id', + 'class', + 'rel', + 'tabindex', + 'title', + 'target', + 'role', +]; +const attributeToPropertyMap = { + role: 'ariaRole', +}; + +class AngleBracketLinkToPolyfill { + transform(ast) { + let b = this.syntax.builders; + + // in order to debug in https://https://astexplorer.net/#/gist/0590eb883edfcd163b183514df4cc717 + // **** copy from here **** + function transformAttributeValue(attributeValue) { + switch (attributeValue.type) { + case 'TextNode': + return b.string(attributeValue.chars); + case 'MustacheStatement': + return b.path(attributeValue.path); + } + } + + let visitor = { + ElementNode(node) { + if (node.tag.toLowerCase() === 'linkto') { + + let { children, blockParams, attributes } = node; + let params = []; + let helperParams = []; + + let route = attributes.find(({ name }) => name === '@route'); + let model = attributes.find(({ name }) => name === '@model'); + let models = attributes.find(({ name }) => name === '@models'); + let query = attributes.find(({ name }) => name === '@query'); + + let needsParamsHelper = (models && models.value.path.original !== 'array') || (query && query.value.path.original !== 'hash'); + + if (route) { + if (needsParamsHelper) { + helperParams.push(b.pair('route', transformAttributeValue(route.value))); + } else { + params.push(transformAttributeValue(route.value)); + } + } + + if (model) { + if (needsParamsHelper) { + helperParams.push(b.pair('model', transformAttributeValue(model.value))); + } else { + params.push(transformAttributeValue(model.value)); + } + } + + if (models) { + if (models.value.path.original === 'array') { + params.push(...models.value.params); + } else { + helperParams.push(b.pair('models', transformAttributeValue(models.value))); + } + } + + if (query) { + if (query.value.path.original === 'hash') { + params.push(b.sexpr('query-params', null, query.value.hash, query.loc)); + } else { + helperParams.push(b.pair('query', transformAttributeValue(query.value))); + } + } + + let props = attributes + .filter(({ name }) => name.charAt(0) === '@' && !reservedProps.includes(name)) + .map((attribute) => Object.assign({}, attribute, { name: attribute.name.slice(1) })); + let attrs = attributes + .filter(({ name }) => supportedHTMLAttributes.includes(name)) + .map((attribute) => attributeToPropertyMap[attribute.name] ? Object.assign({}, attribute, { name: attributeToPropertyMap[attribute.name] }) : attribute); + + let hash = b.hash( + [...props, ...attrs] + .map(({ name, value, loc }) => b.pair(name, transformAttributeValue(value), loc)) + ); + + if (needsParamsHelper) { + hash.pairs.push(b.pair('params', + b.sexpr( + '-link-to-params', + null, + b.hash(helperParams) + ))); + + return b.block( + b.path('link-to'), + null, + hash, + b.program(children, blockParams), + null, + node.loc + ); + } else { + return b.block( + b.path('link-to'), + params, + hash, + b.program(children, blockParams), + null, + node.loc + ); + } + } + } + }; + // **** copy to here **** + + this.syntax.traverse(ast, visitor); + + return ast; + } +} + +module.exports = AngleBracketLinkToPolyfill; diff --git a/tests/integration/components/link-to-test.js b/tests/integration/components/link-to-test.js new file mode 100644 index 0000000..34488e5 --- /dev/null +++ b/tests/integration/components/link-to-test.js @@ -0,0 +1,109 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; +import EmberRouter from '@ember/routing/router'; +import EmberObject from '@ember/object'; + +module('Integration | Component | link-to', function(hooks) { + const Router = EmberRouter.extend(); + Router.map(function() { + this.route('foo'); + this.route('bar', { path: '/bar/:bar_id' }, function() { + this.route('sub', { path: '/sub/:sub_id' }); + }); + }); + + const modelA = EmberObject.create({ + id: '1', + }); + const modelB = EmberObject.create({ + id: '2', + }); + + setupRenderingTest(hooks); + hooks.beforeEach(function() { + this.owner.register('router:main', Router); + this.owner.setupRouter(); + this.setProperties({ + modelA, + modelB, + }); + }); + + test('it supports static route', async function(assert) { + await render(hbs`Link`); + + assert.dom('a').hasAttribute('href', '#/foo'); + assert.dom('a').hasClass('main'); + assert.dom('a').hasText('Link'); + }); + + test('it supports dynamic route', async function(assert) { + await render(hbs`Link`); + + assert.dom('a').hasAttribute('href', '#/bar/1'); + assert.dom('a').hasClass('main'); + assert.dom('a').hasText('Link'); + }); + + test('it supports dynamic route w/ multiple models', async function(assert) { + this.set('models', [this.modelA, this.modelB]); + await render(hbs`Link`); + + assert.dom('a').hasAttribute('href', '#/bar/1/sub/2'); + assert.dom('a').hasClass('main'); + assert.dom('a').hasText('Link'); + }); + + test('it supports dynamic route w/ multiple models and array helper', async function(assert) { + this.set('models', [this.modelA, this.modelB]); + await render( + hbs`Link` + ); + + assert.dom('a').hasAttribute('href', '#/bar/1/sub/2'); + assert.dom('a').hasClass('main'); + assert.dom('a').hasText('Link'); + }); + + test('it supports query params', async function(assert) { + this.set('query', { + q1: 1, + q2: 'some', + q3: 'value', + }); + await render(hbs`Link`); + + assert.dom('a').hasAttribute('href', '#/foo?q1=1&q2=some&q3=value'); + assert.dom('a').hasClass('main'); + assert.dom('a').hasText('Link'); + }); + + test('it supports query params w/ hash helper', async function(assert) { + this.set('q3', 'value'); + await render(hbs`Link`); + + assert.dom('a').hasAttribute('href', '#/foo?q1=1&q2=some&q3=value'); + assert.dom('a').hasClass('main'); + assert.dom('a').hasText('Link'); + }); + + test('it passes supported properties and attributes', async function(assert) { + this.owner.startRouting(); + + await render( + hbs`Link` + ); + assert.dom('a').hasAttribute('href', '#/'); + assert.dom('a').hasClass('act'); + assert.dom('a').hasClass('main'); + assert.dom('a').hasAttribute('id', 'test'); + assert.dom('a').hasAttribute('role', 'nav'); + assert.dom('a').hasAttribute('rel', 'noopener'); + assert.dom('a').hasAttribute('tabindex', '1'); + assert.dom('a').hasAttribute('title', 'something'); + assert.dom('a').hasAttribute('target', '_blank'); + assert.dom('a').hasText('Link'); + }); +});