Skip to content

Commit

Permalink
Add polyfill for angle brackets <LinkTo>
Browse files Browse the repository at this point in the history
Partially implements #66
  • Loading branch information
simonihmig committed Jul 12, 2019
1 parent ca97c03 commit 89eff29
Show file tree
Hide file tree
Showing 6 changed files with 307 additions and 3 deletions.
6 changes: 3 additions & 3 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions addon/helpers/-link-to-params.js
Original file line number Diff line number Diff line change
@@ -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);
1 change: 1 addition & 0 deletions app/helpers/-link-to-params.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from 'ember-angle-bracket-invocation-polyfill/helpers/-link-to-params';
33 changes: 33 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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() {
Expand All @@ -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);

Expand Down Expand Up @@ -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);
}
},
};
133 changes: 133 additions & 0 deletions lib/ast-link-to-transform.js
Original file line number Diff line number Diff line change
@@ -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;
109 changes: 109 additions & 0 deletions tests/integration/components/link-to-test.js
Original file line number Diff line number Diff line change
@@ -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`<LinkTo @route="foo" class="main">Link</LinkTo>`);

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`<LinkTo @route="bar" @model={{this.modelA}} class="main">Link</LinkTo>`);

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`<LinkTo @route="bar.sub" @models={{this.models}} class="main">Link</LinkTo>`);

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`<LinkTo @route="bar.sub" @models={{array this.modelA this.modelB}} class="main">Link</LinkTo>`
);

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`<LinkTo @route="foo" @query={{this.query}} class="main">Link</LinkTo>`);

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`<LinkTo @route="foo" @query={{hash q1=1 q2="some" q3=this.q3}} class="main">Link</LinkTo>`);

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`<LinkTo @route="index" @activeClass="act" class="main" id="test" role="nav" rel="noopener" tabindex="1" title="something" target="_blank">Link</LinkTo>`
);
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');
});
});

0 comments on commit 89eff29

Please sign in to comment.