Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add polyfill for <Input> and <Textarea> #75

Merged
merged 1 commit into from
Jul 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,21 @@ module.exports = {
}

if (this.shouldPolyfillBuiltinComponents) {
let pluginObj = this._buildLinkToPlugin();
pluginObj.parallelBabel = {
let linktoPluginObj = this._buildLinkToPlugin();
linktoPluginObj.parallelBabel = {
requireFile: __filename,
buildUsing: '_buildLinkToPlugin',
params: {},
};
registry.add('htmlbars-ast-plugin', pluginObj);
registry.add('htmlbars-ast-plugin', linktoPluginObj);

let inputPluginObj = this._buildInputPlugin();
inputPluginObj.parallelBabel = {
requireFile: __filename,
buildUsing: '_buildInputPlugin',
params: {},
};
registry.add('htmlbars-ast-plugin', inputPluginObj);
}
},

Expand Down Expand Up @@ -86,6 +94,16 @@ module.exports = {
};
},

_buildInputPlugin() {
return {
name: 'input-component-invocation-support',
plugin: require('./lib/ast-input-transform'),
baseDir() {
return __dirname;
},
};
},

included() {
this._super.included.apply(this, arguments);

Expand Down
56 changes: 56 additions & 0 deletions lib/ast-input-transform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use strict';

const attributeToPropertyMap = {
role: 'ariaRole',
};

class AngleBracketInputPolyfill {
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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let’s propagate the original location info in these branches (so downstream error messages can use it)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems I cannot do that with b.string(), can I?

https://github.com/glimmerjs/glimmer-vm/blob/master/packages/%40glimmer/syntax/lib/builders.ts#L564

Btw, as AST docs are quite sparse/non-existent, what does this actually do and how is this used? For error messages to print concrete line/column numbers of hbs files that break something?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems I cannot do that with b.string(), can I?

Ugg, indeed you are correct. Though I think you can create it and then add the loc:

let newString = b.string(attributeValue.chars);
newString.loc = attributeValue.loc;
return newString;

https://github.com/glimmerjs/glimmer-vm/blob/83a0c2fdfa8f22fd353dcab32ea41c4bcacaa379/packages/%40glimmer/syntax/lib/builders.ts#L577-L581 needs to be updated to take loc as the last arg in the returned function.

what does this actually do and how is this used? For error messages to print concrete line/column numbers of hbs files that break something?

Right, when creating new AST nodes we should always attempt to propagate the original location information through to the new nodes. This helps to ensure that any error messages include the correct module name, line, and column number.

case 'MustacheStatement':
return b.path(attributeValue.path);
}
}

let visitor = {
ElementNode(node) {
let tag = node.tag.toLowerCase();

if (tag === 'input' || tag === 'textarea') {
let { attributes } = node;

let props = attributes
.filter(({ name }) => name.charAt(0) === '@')
.map(attribute => Object.assign({}, attribute, { name: attribute.name.slice(1) }));
let attrs = attributes.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)
)
);

return b.mustache(b.path(tag), null, hash, false, node.loc);
}
},
};
// **** copy to here ****

this.syntax.traverse(ast, visitor);

return ast;
}
}

module.exports = AngleBracketInputPolyfill;
62 changes: 62 additions & 0 deletions tests/integration/components/input-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';

module('Integration | Component | input', function(hooks) {
setupRenderingTest(hooks);

test('it supports text field', async function(assert) {
this.set('value', 'foo');
await render(hbs`<Input @type="text" @value={{this.value}} />`);

assert.dom('input').hasAttribute('type', 'text');
assert.dom('input').hasValue('foo');
});

test('it supports different input types', async function(assert) {
this.set('value', 'user@example.com');
await render(hbs`<Input @type="email" @value={{this.value}} />`);

assert.dom('input').hasAttribute('type', 'email');
assert.dom('input').hasValue('user@example.com');
});

test('it supports checkbox', async function(assert) {
this.set('value', true);
await render(hbs`<Input @type="checkbox" @checked={{this.value}} />`);

assert.dom('input').hasAttribute('type', 'checkbox');
assert.dom('input').isChecked();
});

test('it supports textarea', async function(assert) {
this.set('value', 'foo bar');
await render(hbs`<Textarea @value={{this.value}} />`);

assert.dom('textarea').exists();
assert.dom('textarea').hasValue('foo bar');
});

test('it passes supported properties and attributes', async function(assert) {
await render(
hbs`<Input
@value="foo"
@size="20"
name="username"
placeholder="Enter username"
class="form-input"
role="searchbox"
/>`
);

// does not test each and every supported property / HTML attribute, as the list is rather long, and the transform
// will just pass anything through, so either all work or no one does.
assert.dom('input').hasAttribute('type', 'text');
assert.dom('input').hasValue('foo');
assert.dom('input').hasAttribute('name', 'username');
assert.dom('input').hasAttribute('placeholder', 'Enter username');
assert.dom('input').hasClass('form-input');
assert.dom('input').hasAttribute('role', 'searchbox');
});
});