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 {{macroMaybeModifier}} template macro #692

Closed
wants to merge 2 commits into from
Closed
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
32 changes: 19 additions & 13 deletions packages/macros/src/glimmer/ast-transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import dependencySatisfies from './dependency-satisfies';
import { maybeAttrs } from './macro-maybe-attrs';
import { macroIfBlock, macroIfExpression, macroIfMustache } from './macro-condition';
import { failBuild } from './fail-build';
import { maybeModifier } from './macro-maybe-modifier';

export function makeFirstTransform(opts: { userConfigs: { [packageRoot: string]: unknown }; baseDir?: string }) {
function embroiderFirstMacrosTransform(env: {
Expand Down Expand Up @@ -140,19 +141,24 @@ export function makeSecondTransform() {
}
},
ElementNode(node: any) {
node.modifiers = node.modifiers.filter((modifier: any) => {
if (modifier.path.type !== 'PathExpression') {
return true;
}
if (inScope(scopeStack, modifier.path.parts[0])) {
return true;
}
if (modifier.path.original === 'macroMaybeAttrs') {
maybeAttrs(node, modifier, env.syntax.builders);
} else {
return true;
}
});
node.modifiers = node.modifiers
.map((modifier: any) => {
if (modifier.path.type !== 'PathExpression') {
return modifier;
}
if (inScope(scopeStack, modifier.path.parts[0])) {
return modifier;
}
if (modifier.path.original === 'macroMaybeAttrs') {
maybeAttrs(node, modifier, env.syntax.builders);
return false;
}
if (modifier.path.original === 'macroMaybeModifier') {
return maybeModifier(modifier, env.syntax.builders);
}
return modifier;
})
.filter(Boolean);
},
MustacheStatement(node: any) {
if (node.path.type !== 'PathExpression') {
Expand Down
24 changes: 24 additions & 0 deletions packages/macros/src/glimmer/macro-maybe-modifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import evaluate from './evaluate';

export function maybeModifier(node: any, builders: any) {
let [predicate, originalModifier, ...positionalArgs] = node.params;

if (!predicate) {
throw new Error(`macroMaybeModifier requires at least one argument`);
}

let result = evaluate(predicate);
if (!result.confident) {
throw new Error(`first argument to macroMaybeModifier must be statically analyzable`);
}

if (originalModifier.type !== 'PathExpression') {
throw new Error(`macroMaybeModifier found a ${originalModifier.type} where it expected a PathExpression`);
}

if (result.value) {
return builders.elementModifier(originalModifier, positionalArgs, node.hash);
} else {
return false;
}
}
35 changes: 35 additions & 0 deletions packages/macros/tests/glimmer/macro-maybe-modifier.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { templateTests } from './helpers';

describe(`macroMaybeModifier`, function () {
templateTests(function (transform: (code: string) => string) {
test('macroMaybeModifier when true', function () {
let code = transform(`<button {{macroMaybeModifier true on "click" this.submit}} ></button>`);
expect(code).toMatch(/<button {{on "click" this.submit}}>/);
});

test('macroMaybeModifier propagates named args', function () {
let code = transform(`<button {{macroMaybeModifier true on "click" this.submit passive=true}} ></button>`);
expect(code).toMatch(/<button {{on "click" this.submit passive=true}}>/);
});

test('macroMaybeModifier propagates bound paths', function () {
let code = transform(
`<button {{macroMaybeModifier true on this.event this.submit passive=this.passive}} ></button>`
);
expect(code).toMatch(/<button {{on this.event this.submit passive=this.passive}}>/);
});

test('macroMaybeModifier when false', function () {
let code = transform(`<button {{macroMaybeModifier false on "click" this.submit}} ></button>`);
expect(code).toMatch(/<button>/);
});

test('macroMaybeModifier leaves other modifiers alone', function () {
let code = transform(
`<button {{macroMaybeModifier false on "click" this.submit}} {{style color="red"}}></button>`
);

expect(code).toMatch(/<button {{style color="red"}}>/);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,126 +1,97 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, click } from '@ember/test-helpers';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import { helper } from '@ember/component/helper';

module('Integration | Macro | macroCondition', function(hooks) {
module('Integration | Macro | macroCondition', function (hooks) {
setupRenderingTest(hooks);

test('macroCondition in content position when true', async function(assert) {
test('macroCondition in content position when true', async function (assert) {
await render(hbs`{{#if (macroCondition true)}}red{{else}}blue{{/if}}`);
assert.equal(this.element.textContent.trim(), 'red');
});

test('macroCondition in content position when false', async function(assert) {
test('macroCondition in content position when false', async function (assert) {
await render(hbs`{{#if (macroCondition false)}}red{{else}}blue{{/if}}`);
assert.equal(this.element.textContent.trim(), 'blue');
});

test('macroCondition in content position when false with no alternate', async function(assert) {
test('macroCondition in content position when false with no alternate', async function (assert) {
await render(hbs`{{#if (macroCondition false)}}red{{/if}}`);
assert.equal(this.element.textContent.trim(), '');
});

test('macroCondition in subexpression position when true', async function(assert) {
test('macroCondition in subexpression position when true', async function (assert) {
assert.expect(1);
this.owner.register(
'helper:my-assertion',
helper(function([value]) {
helper(function ([value]) {
assert.strictEqual(value, 'red');
})
);
await render(hbs`{{my-assertion (if (macroCondition true) 'red' 'blue') }}`);
});

test('macroCondition inside string', async function(assert) {
test('macroCondition inside string', async function (assert) {
assert.expect(1);
await render(hbs`<div class="target {{if (macroCondition true) 'red' 'blue' }}"></div>`);
assert.ok(this.element.querySelector('.target').matches('.red'));
});

test('macroCondition in subexpression position when false', async function(assert) {
test('macroCondition in subexpression position when false', async function (assert) {
assert.expect(1);
this.owner.register(
'helper:my-assertion',
helper(function([value]) {
helper(function ([value]) {
assert.strictEqual(value, 'blue');
})
);
await render(hbs`{{my-assertion (if (macroCondition false) 'red' 'blue') }}`);
});

test('macroCondition in subexpression position when false with no alternate', async function(assert) {
test('macroCondition in subexpression position when false with no alternate', async function (assert) {
assert.expect(1);
this.owner.register(
'helper:my-assertion',
helper(function([value]) {
helper(function ([value]) {
assert.strictEqual(value, undefined);
})
);
await render(hbs`{{my-assertion (if (macroCondition false) 'red') }}`);
});

test('macroMaybeAttrs when true', async function(assert) {
await render(hbs`<div data-test-target {{macroMaybeAttrs true data-optional data-flavor="vanilla" }} ></div>`);
let target = this.element.querySelector('[data-test-target]');
assert.ok(target.matches('[data-optional]'), 'found data-optional');
assert.ok(target.matches('[data-flavor="vanilla"]'), 'found data-flavor');
});

test('macroMaybeAttrs propagates bound paths', async function(assert) {
this.set('flavor', 'vanilla');
await render(hbs`<div data-test-target {{macroMaybeAttrs true data-flavor=this.flavor }} ></div>`);
let target = this.element.querySelector('[data-test-target]');
assert.ok(target.matches('[data-flavor="vanilla"]'), 'found data-flavor');
});

test('macroMaybeAttrs when false', async function(assert) {
await render(hbs`<div data-test-target {{macroMaybeAttrs false data-optional data-flavor="vanilla" }} ></div>`);
let target = this.element.querySelector('[data-test-target]');
assert.ok(!target.matches('[data-optional]'));
assert.ok(!target.matches('[data-flavor="vanilla"]'));
});

test('macroMaybeAttrs leaves other modifiers alone', async function(assert) {
assert.expect(1);
this.doThing = function() {
assert.ok(true, 'it ran');
};
await render(
hbs`<div data-test-target {{action doThing}} {{macroMaybeAttrs false data-optional data-flavor="vanilla" }} ></div>`
);
let target = this.element.querySelector('[data-test-target]');
await click(target);
});

test('macroCondition composes with other macros, true case', async function(assert) {
test('macroCondition composes with other macros, true case', async function (assert) {
assert.expect(1);
this.owner.register(
'helper:my-assertion',
helper(function([value]) {
helper(function ([value]) {
assert.strictEqual(value, 'red');
})
);
await render(hbs`{{my-assertion (if (macroCondition (macroDependencySatisfies 'ember-source' '3.x')) 'red' 'blue') }}`);
await render(
hbs`{{my-assertion (if (macroCondition (macroDependencySatisfies 'ember-source' '3.x')) 'red' 'blue') }}`
);
});

test('macroCondition composes with other macros, false case', async function(assert) {
test('macroCondition composes with other macros, false case', async function (assert) {
assert.expect(1);
this.owner.register(
'helper:my-assertion',
helper(function([value]) {
helper(function ([value]) {
assert.strictEqual(value, 'blue');
})
);
await render(hbs`{{my-assertion (if (macroCondition (macroDependencySatisfies 'ember-source' '10.x')) 'red' 'blue') }}`);
await render(
hbs`{{my-assertion (if (macroCondition (macroDependencySatisfies 'ember-source' '10.x')) 'red' 'blue') }}`
);
});

test('macroCondition composes with self', async function(assert) {
test('macroCondition composes with self', async function (assert) {
assert.expect(1);
this.owner.register(
'helper:my-assertion',
helper(function([value]) {
helper(function ([value]) {
assert.strictEqual(value, 'red');
})
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { click, render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';

module('Integration | Macro | macroMaybeAttrs', function (hooks) {
setupRenderingTest(hooks);

test('macroMaybeAttrs when true', async function (assert) {
await render(hbs`<div data-test-target {{macroMaybeAttrs true data-optional data-flavor="vanilla" }} ></div>`);
let target = this.element.querySelector('[data-test-target]');
assert.ok(target.matches('[data-optional]'), 'found data-optional');
assert.ok(target.matches('[data-flavor="vanilla"]'), 'found data-flavor');
});

test('macroMaybeAttrs propagates bound paths', async function (assert) {
this.set('flavor', 'vanilla');
await render(hbs`<div data-test-target {{macroMaybeAttrs true data-flavor=this.flavor }} ></div>`);
let target = this.element.querySelector('[data-test-target]');
assert.ok(target.matches('[data-flavor="vanilla"]'), 'found data-flavor');
});

test('macroMaybeAttrs when false', async function (assert) {
await render(hbs`<div data-test-target {{macroMaybeAttrs false data-optional data-flavor="vanilla" }} ></div>`);
let target = this.element.querySelector('[data-test-target]');
assert.ok(!target.matches('[data-optional]'));
assert.ok(!target.matches('[data-flavor="vanilla"]'));
});

test('macroMaybeAttrs leaves other modifiers alone', async function (assert) {
assert.expect(1);
this.doThing = function () {
assert.ok(true, 'it ran');
};
await render(
hbs`<div data-test-target {{action doThing}} {{macroMaybeAttrs false data-optional data-flavor="vanilla" }} ></div>`
);
let target = this.element.querySelector('[data-test-target]');
await click(target);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { click, render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';

module('Integration | Macro | macroMaybeModifier', function (hooks) {
setupRenderingTest(hooks);

test('macroMaybeModifier when true', async function (assert) {
let called = false;
this.set('action', () => (called = true));
await render(hbs`<button data-test-target {{macroMaybeModifier true action this.action}}></button>`);
await click('[data-test-target]');
assert.ok(called, 'action modifier works');
});

test('macroMaybeModifier when false', async function (assert) {
let called = false;
this.set('action', () => (called = true));
await render(hbs`<button data-test-target {{macroMaybeModifier false action this.action}}></button>`);
await click('[data-test-target]');
assert.notOk(called, 'action modifier was not applied');
});

test('macroMaybeModifier leaves other modifiers alone', async function (assert) {
let called = false;
this.set('action', () => (called = true));
await render(
hbs`<div data-test-target {{action this.action}} {{macroMaybeModifier false action this.someOtherAction}} ></div>`
);
await click('[data-test-target]');
assert.ok(called, 'other modifier works');
});
});