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

toucan-form: Expose named blocks for textarea #149

Merged
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
95 changes: 81 additions & 14 deletions packages/ember-toucan-form/src/-private/textarea-field.hbs
Copy link
Contributor Author

@ynotdraw ynotdraw Apr 25, 2023

Choose a reason for hiding this comment

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

I described the issue in the comment here, but TL;DR:

The issue is we cannot do something like:

<@form.Field @name={{@name}} as |field|>
  <Form::Fields::Textarea
    @hint={{@hint}}
    @error={{this.mapErrors field.rawErrors}}
    @value={{this.assertString field.value}}
    {{! @glint-expect-error }}
    @onChange={{field.setValue}}
    @isDisabled={{@isDisabled}}
    @isReadOnly={{@isReadOnly}}
    @rootTestSelector={{@rootTestSelector}}
    name={{@name}}
    ...attributes
  >
    {{#if (has-block 'label')}}
      <:label>{{yield to='label'}}</:label>
    {{/if}}
  </Form::Fields::Textarea>
</@form.Field>

Or else we get the following due to emberjs/rfcs#735:

Build Error (PackagerRunner) in ../packages/ember-toucan-form/dist/-private/textarea-field.js

Module build failed (from ember-toucan-core/node_modules/.pnpm/thread-loader@3.0.4_webpack@5.75.0/node_modules/thread-loader/dist/cjs.js):
Thread Loader (Worker 5)
$TMPDIR/embroider/835ae7/packages/ember-toucan-form/dist/-private/textarea-field.js/textarea-field.js: Unexpected named block nested in a normal block:

|
|
|        <:label>{{yield to='label'}}</:label>
|
|

(error occurred in '$TMPDIR/embroider/835ae7/packages/ember-toucan-form/dist/-private/textarea-field.js' @ line 33 : column 31)

Essentially we have two layers of named blocks here:

  1. In Form::Fields::Textarea - this is the source of the named blocks
  2. When users use our <form.Textarea component in toucan-form

Due to this nesting, essentially passing blocks from a higher level component to a lower level one does not work as you'd expect. We essentially need something like splat-blocks or something I think to solve this?

Original file line number Diff line number Diff line change
@@ -1,16 +1,83 @@
{{!
Regarding Conditionals

This looks really messy, but Form::Fields::Textarea exposes named blocks; HOWEVER,
we cannot conditionally render named blocks due to /~https://github.com/emberjs/rfcs/issues/735.

We *can* conditionally render components though, based on the blocks and argument combinations
users provide us. This is very brittle, but until /~https://github.com/emberjs/rfcs/issues/735
is resolved and a solution is found, this appears to be the only way to truly expose
conditional named blocks.

---

Regarding glint-expect-error

"@onChange" of the textarea only expects a string typed value, but field.setValue is generic,
accepting anything that DATA[KEY] could be. Similar case with "@value", but there casting to
a string is easy.
}}
<@form.Field @name={{@name}} as |field|>
<Form::Fields::Textarea
@label={{@label}}
@hint={{@hint}}
@error={{this.mapErrors field.rawErrors}}
@value={{this.assertString field.value}}
{{! The issue here is that onChange of textarea only expects a string typed value, but field.setValue is generic, accepting anything that DATA[KEY] could be. Similar case with @value, but there casting to a string is easy. }}
{{! @glint-expect-error }}
@onChange={{field.setValue}}
@isDisabled={{@isDisabled}}
@isReadOnly={{@isReadOnly}}
@rootTestSelector={{@rootTestSelector}}
name={{@name}}
...attributes
/>
{{#if (this.hasOnlyLabelBlock (has-block 'label') (has-block 'hint'))}}
<Form::Fields::Textarea
@hint={{@hint}}
@error={{this.mapErrors field.rawErrors}}
@value={{this.assertString field.value}}
{{! @glint-expect-error }}
@onChange={{field.setValue}}
@isDisabled={{@isDisabled}}
@isReadOnly={{@isReadOnly}}
@rootTestSelector={{@rootTestSelector}}
name={{@name}}
...attributes
>
<:label>{{yield to='label'}}</:label>
</Form::Fields::Textarea>
{{else if (this.hasHintAndLabelBlocks (has-block 'label') (has-block 'hint'))
}}
<Form::Fields::Textarea
@error={{this.mapErrors field.rawErrors}}
@value={{this.assertString field.value}}
{{! @glint-expect-error }}
@onChange={{field.setValue}}
@isDisabled={{@isDisabled}}
@isReadOnly={{@isReadOnly}}
@rootTestSelector={{@rootTestSelector}}
name={{@name}}
...attributes
>
<:label>{{yield to='label'}}</:label>
<:hint>{{yield to='hint'}}</:hint>
</Form::Fields::Textarea>
{{else if (this.hasLabelArgAndHintBlock @label (has-block 'hint'))}}
<Form::Fields::Textarea
@label={{@label}}
@error={{this.mapErrors field.rawErrors}}
@value={{this.assertString field.value}}
{{! @glint-expect-error }}
@onChange={{field.setValue}}
@isDisabled={{@isDisabled}}
@isReadOnly={{@isReadOnly}}
@rootTestSelector={{@rootTestSelector}}
name={{@name}}
...attributes
>
<:hint>{{yield to='hint'}}</:hint>
</Form::Fields::Textarea>
{{else}}
{{! Argument-only case }}
<Form::Fields::Textarea
@label={{@label}}
@hint={{@hint}}
@error={{this.mapErrors field.rawErrors}}
@value={{this.assertString field.value}}
{{! @glint-expect-error }}
@onChange={{field.setValue}}
@isDisabled={{@isDisabled}}
@isReadOnly={{@isReadOnly}}
@rootTestSelector={{@rootTestSelector}}
name={{@name}}
...attributes
/>
{{/if}}
</@form.Field>
11 changes: 8 additions & 3 deletions packages/ember-toucan-form/src/-private/textarea-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,20 @@ export interface ToucanFormTextareaFieldComponentSignature<
*/
form: HeadlessFormBlock<DATA>;
};
Blocks: {
default: [];
};
Blocks: BaseTextareaFieldSignature['Blocks'];
}

export default class ToucanFormTextareaFieldComponent<
DATA extends UserData,
KEY extends FormKey<FormData<DATA>> = FormKey<FormData<DATA>>
> extends Component<ToucanFormTextareaFieldComponentSignature<DATA, KEY>> {
hasOnlyLabelBlock = (hasLabel: boolean, hasHint: boolean) =>
hasLabel && !hasHint;
hasHintAndLabelBlocks = (hasLabel: boolean, hasHint: boolean) =>
hasLabel && hasHint;
hasLabelArgAndHintBlock = (hasLabel: string | undefined, hasHint: boolean) =>
hasLabel && hasHint;

mapErrors = (errors?: ValidationError[]) => {
if (!errors) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,77 @@ module('Integration | Component | ToucanForm | Textarea', function (hooks) {

assert.dom('[data-textarea]').hasAttribute('readonly');
});

test('it renders `@label` and `@hint` component arguments', async function (assert) {
const data: TestData = {
text: 'multi-line text',
};

await render(<template>
<ToucanForm @data={{data}} as |form|>
<form.Textarea @label="Label" @hint="Hint" @name="text" />
</ToucanForm>
</template>);

assert.dom('[data-label]').exists();
assert.dom('[data-hint]').exists();
});

test('it renders a `:label` named block with a `@hint` argument', async function (assert) {
const data: TestData = {
text: 'multi-line text',
};

await render(<template>
<ToucanForm @data={{data}} as |form|>
<form.Textarea @name="text" @hint="Hint">
<:label><span data-label-block>Label</span></:label>
</form.Textarea>
</ToucanForm>
</template>);

assert.dom('[data-label-block]').exists();

// NOTE: `data-hint` comes from `@hint`.
assert.dom('[data-hint]').exists();
assert.dom('[data-hint]').hasText('Hint');
});

test('it renders a `:hint` named block with a `@label` argument', async function (assert) {
const data: TestData = {
text: 'multi-line text',
};

await render(<template>
<ToucanForm @data={{data}} as |form|>
<form.Textarea @label="Label" @name="text">
<:hint><span data-hint-block>Hint</span></:hint>
</form.Textarea>
</ToucanForm>
</template>);

// NOTE: `data-label` comes from `@label`.
assert.dom('[data-label]').exists();
assert.dom('[data-label]').hasText('Label');

assert.dom('[data-hint-block]').exists();
});

test('it renders both a `:label` and `:hint` named block', async function (assert) {
const data: TestData = {
text: 'multi-line text',
};

await render(<template>
<ToucanForm @data={{data}} as |form|>
<form.Textarea @label="Label" @name="text">
<:label><span data-label-block>Label</span></:label>
<:hint><span data-hint-block>Hint</span></:hint>
</form.Textarea>
</ToucanForm>
</template>);

assert.dom('[data-label-block]').exists();
assert.dom('[data-hint-block]').exists();
});
});