diff --git a/.changeset/light-olives-notice.md b/.changeset/light-olives-notice.md new file mode 100644 index 000000000..d54fd3057 --- /dev/null +++ b/.changeset/light-olives-notice.md @@ -0,0 +1,38 @@ +--- +'@crowdstrike/ember-toucan-form': patch +--- + +Exposed named blocks from the underlying `toucan-core` components. This allows users to add custom content in `:hint` or `:label` named blocks. You can combine the arguments and named blocks as well! Below are some examples. + +```hbs +<ToucanForm @data={{data}} as |form|> + <form.Textarea @name='comment'> + <:label>Label</:label> + <:hint>Hint</:hint> + </form.Textarea> +</ToucanForm> +``` + +```hbs +<ToucanForm @data={{data}} as |form|> + <form.Textarea @label='Label' @name='comment'> + <:hint>Hint</:hint> + </form.Textarea> +</ToucanForm> +``` + +```hbs +<ToucanForm @data={{data}} as |form|> + <form.Textarea @hint='Hint' @name='comment'> + <:label>Label</:label> + </form.Textarea> +</ToucanForm> +``` + +Or you can continue to use the arguments if you're only working with strings! + +```hbs +<ToucanForm @data={{data}} as |form|> + <form.Textarea @label='Label' @hint='Hint' @name='comment' /> +</ToucanForm> +``` diff --git a/packages/ember-toucan-form/src/-private/checkbox-field.hbs b/packages/ember-toucan-form/src/-private/checkbox-field.hbs index 66e3ea7e1..750b545a9 100644 --- a/packages/ember-toucan-form/src/-private/checkbox-field.hbs +++ b/packages/ember-toucan-form/src/-private/checkbox-field.hbs @@ -1,16 +1,83 @@ +{{! + Regarding Conditionals + + This looks really messy, but Form::Fields::Checkbox 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 checkbox only expects a boolean typed value, but field.setValue is generic, + accepting anything that DATA[KEY] could be. Similar case with "@isChecked", but there casting to + a boolean is easy. +}} <@form.Field @name={{@name}} as |field|> - <Form::Fields::Checkbox - @label={{@label}} - @hint={{@hint}} - @error={{this.mapErrors field.rawErrors}} - @isChecked={{this.assertBoolean field.value}} - {{! The issue here is that onChange only expects a string typed value, but field.setValue is generic, accepting anything that DATA[KEY] could be. }} - {{! @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::Checkbox + @hint={{@hint}} + @error={{this.mapErrors field.rawErrors}} + @isChecked={{this.assertBoolean field.value}} + {{! @glint-expect-error }} + @onChange={{field.setValue}} + @isDisabled={{@isDisabled}} + @isReadOnly={{@isReadOnly}} + @rootTestSelector={{@rootTestSelector}} + name={{@name}} + ...attributes + > + <:label>{{yield to='label'}}</:label> + </Form::Fields::Checkbox> + {{else if (this.hasHintAndLabelBlocks (has-block 'label') (has-block 'hint')) + }} + <Form::Fields::Checkbox + @error={{this.mapErrors field.rawErrors}} + @isChecked={{this.assertBoolean 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::Checkbox> + {{else if (this.hasLabelArgAndHintBlock @label (has-block 'hint'))}} + <Form::Fields::Checkbox + @label={{@label}} + @error={{this.mapErrors field.rawErrors}} + @isChecked={{this.assertBoolean field.value}} + {{! @glint-expect-error }} + @onChange={{field.setValue}} + @isDisabled={{@isDisabled}} + @isReadOnly={{@isReadOnly}} + @rootTestSelector={{@rootTestSelector}} + name={{@name}} + ...attributes + > + <:hint>{{yield to='hint'}}</:hint> + </Form::Fields::Checkbox> + {{else}} + {{! Argument-only case }} + <Form::Fields::Checkbox + @label={{@label}} + @hint={{@hint}} + @error={{this.mapErrors field.rawErrors}} + @isChecked={{this.assertBoolean field.value}} + {{! @glint-expect-error }} + @onChange={{field.setValue}} + @isDisabled={{@isDisabled}} + @isReadOnly={{@isReadOnly}} + @rootTestSelector={{@rootTestSelector}} + name={{@name}} + ...attributes + /> + {{/if}} </@form.Field> \ No newline at end of file diff --git a/packages/ember-toucan-form/src/-private/checkbox-field.ts b/packages/ember-toucan-form/src/-private/checkbox-field.ts index fa72d6391..90ec65932 100644 --- a/packages/ember-toucan-form/src/-private/checkbox-field.ts +++ b/packages/ember-toucan-form/src/-private/checkbox-field.ts @@ -25,15 +25,20 @@ export interface ToucanFormCheckboxFieldComponentSignature< */ form: HeadlessFormBlock<DATA>; }; - Blocks: { - default: []; - }; + Blocks: BaseCheckboxFieldSignature['Blocks']; } export default class ToucanFormTextareaFieldComponent< DATA extends UserData, KEY extends FormKey<FormData<DATA>> = FormKey<FormData<DATA>> > extends Component<ToucanFormCheckboxFieldComponentSignature<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; diff --git a/packages/ember-toucan-form/src/-private/checkbox-group-field.hbs b/packages/ember-toucan-form/src/-private/checkbox-group-field.hbs index ea2b095c8..bd8a5cb21 100644 --- a/packages/ember-toucan-form/src/-private/checkbox-group-field.hbs +++ b/packages/ember-toucan-form/src/-private/checkbox-group-field.hbs @@ -1,19 +1,95 @@ +{{! + Regarding Conditionals + + This looks really messy, but Form::Fields::CheckboxGroup 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 checkbox-group only expects an array of strings typed value, but field.setValue is generic, + accepting anything that DATA[KEY] could be. Similar case with "@isChecked", but there casting to + an array of strings is easy. +}} <@form.Field @name={{@name}} as |field|> - <Form::Fields::CheckboxGroup - @label={{@label}} - @hint={{@hint}} - @error={{this.mapErrors field.rawErrors}} - @value={{this.assertArrayOfStrings field.value}} - {{! The issue here is that onChange 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 is easy. }} - {{! @glint-expect-error }} - @onChange={{field.setValue}} - @isDisabled={{@isDisabled}} - @isReadOnly={{@isReadOnly}} - @rootTestSelector={{@rootTestSelector}} - @name={{@name}} - ...attributes - as |group| - > - {{yield (hash CheckboxField=group.CheckboxField)}} - </Form::Fields::CheckboxGroup> + {{#if (this.hasOnlyLabelBlock (has-block 'label') (has-block 'hint'))}} + <Form::Fields::CheckboxGroup + @hint={{@hint}} + @error={{this.mapErrors field.rawErrors}} + @value={{this.assertArrayOfStrings field.value}} + {{! @glint-expect-error }} + @onChange={{field.setValue}} + @isDisabled={{@isDisabled}} + @isReadOnly={{@isReadOnly}} + @rootTestSelector={{@rootTestSelector}} + @name={{@name}} + ...attributes + > + <:label>{{yield to='label'}}</:label> + <:default as |group|> + {{yield (hash CheckboxField=group.CheckboxField) to='default'}} + </:default> + </Form::Fields::CheckboxGroup> + {{else if (this.hasHintAndLabelBlocks (has-block 'label') (has-block 'hint')) + }} + <Form::Fields::CheckboxGroup + @error={{this.mapErrors field.rawErrors}} + @value={{this.assertArrayOfStrings 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> + <:default as |group|> + {{yield (hash CheckboxField=group.CheckboxField)}} + </:default> + </Form::Fields::CheckboxGroup> + {{else if (this.hasLabelArgAndHintBlock @label (has-block 'hint'))}} + <Form::Fields::CheckboxGroup + @label={{@label}} + @error={{this.mapErrors field.rawErrors}} + @value={{this.assertArrayOfStrings field.value}} + {{! @glint-expect-error }} + @onChange={{field.setValue}} + @isDisabled={{@isDisabled}} + @isReadOnly={{@isReadOnly}} + @rootTestSelector={{@rootTestSelector}} + @name={{@name}} + ...attributes + > + <:hint>{{yield to='hint'}}</:hint> + <:default as |group|> + {{yield (hash CheckboxField=group.CheckboxField)}} + </:default> + </Form::Fields::CheckboxGroup> + {{else}} + {{! Argument-only case }} + <Form::Fields::CheckboxGroup + @label={{@label}} + @hint={{@hint}} + @error={{this.mapErrors field.rawErrors}} + @value={{this.assertArrayOfStrings field.value}} + {{! @glint-expect-error }} + @onChange={{field.setValue}} + @isDisabled={{@isDisabled}} + @isReadOnly={{@isReadOnly}} + @rootTestSelector={{@rootTestSelector}} + @name={{@name}} + ...attributes + as |group| + > + {{yield (hash CheckboxField=group.CheckboxField)}} + </Form::Fields::CheckboxGroup> + {{/if}} </@form.Field> \ No newline at end of file diff --git a/packages/ember-toucan-form/src/-private/checkbox-group-field.ts b/packages/ember-toucan-form/src/-private/checkbox-group-field.ts index 65f1ac455..64b0e540d 100644 --- a/packages/ember-toucan-form/src/-private/checkbox-group-field.ts +++ b/packages/ember-toucan-form/src/-private/checkbox-group-field.ts @@ -25,19 +25,20 @@ export interface ToucanFormCheckboxGroupFieldComponentSignature< */ form: HeadlessFormBlock<DATA>; }; - Blocks: { - default: [ - { - CheckboxField: BaseCheckboxGroupFieldSignature['Blocks']['default'][0]['CheckboxField']; - } - ]; - }; + Blocks: BaseCheckboxGroupFieldSignature['Blocks']; } export default class ToucanFormTextareaFieldComponent< DATA extends UserData, KEY extends FormKey<FormData<DATA>> = FormKey<FormData<DATA>> > extends Component<ToucanFormCheckboxGroupFieldComponentSignature<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; diff --git a/packages/ember-toucan-form/src/-private/input-field.hbs b/packages/ember-toucan-form/src/-private/input-field.hbs index b2a61b5f1..37952ab5a 100644 --- a/packages/ember-toucan-form/src/-private/input-field.hbs +++ b/packages/ember-toucan-form/src/-private/input-field.hbs @@ -1,16 +1,83 @@ +{{! + Regarding Conditionals + + This looks really messy, but Form::Fields::Input 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 input 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::Input - @label={{@label}} - @hint={{@hint}} - @error={{this.mapErrors field.rawErrors}} - @value={{this.assertString field.value}} - {{! The issue here is that onChange of input 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::Input + @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::Input> + {{else if (this.hasHintAndLabelBlocks (has-block 'label') (has-block 'hint')) + }} + <Form::Fields::Input + @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::Input> + {{else if (this.hasLabelArgAndHintBlock @label (has-block 'hint'))}} + <Form::Fields::Input + @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::Input> + {{else}} + {{! Argument-only case }} + <Form::Fields::Input + @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> \ No newline at end of file diff --git a/packages/ember-toucan-form/src/-private/input-field.ts b/packages/ember-toucan-form/src/-private/input-field.ts index d593bfed5..825990b88 100644 --- a/packages/ember-toucan-form/src/-private/input-field.ts +++ b/packages/ember-toucan-form/src/-private/input-field.ts @@ -25,15 +25,20 @@ export interface ToucanFormInputFieldComponentSignature< */ form: HeadlessFormBlock<DATA>; }; - Blocks: { - default: []; - }; + Blocks: BaseInputFieldSignature['Blocks']; } export default class ToucanFormInputFieldComponent< DATA extends UserData, KEY extends FormKey<FormData<DATA>> = FormKey<FormData<DATA>> > extends Component<ToucanFormInputFieldComponentSignature<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; diff --git a/packages/ember-toucan-form/src/-private/radio-group-field.hbs b/packages/ember-toucan-form/src/-private/radio-group-field.hbs index 828b8303b..c68b4c9b4 100644 --- a/packages/ember-toucan-form/src/-private/radio-group-field.hbs +++ b/packages/ember-toucan-form/src/-private/radio-group-field.hbs @@ -1,19 +1,95 @@ +{{! + Regarding Conditionals + + This looks really messy, but Form::Fields::RadioGroup 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 radio-group only expects a string typed value, but field.setValue is generic, + accepting anything that DATA[KEY] could be. Similar case with "@isChecked", but there casting to + a string is easy. +}} <@form.Field @name={{@name}} as |field|> - <Form::Fields::RadioGroup - @label={{@label}} - @hint={{@hint}} - @error={{this.mapErrors field.rawErrors}} - @value={{this.assertString field.value}} - {{! The issue here is that onChange 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 - as |group| - > - {{yield (hash RadioField=group.RadioField)}} - </Form::Fields::RadioGroup> + {{#if (this.hasOnlyLabelBlock (has-block 'label') (has-block 'hint'))}} + <Form::Fields::RadioGroup + @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> + <:default as |group|> + {{yield (hash RadioField=group.RadioField)}} + </:default> + </Form::Fields::RadioGroup> + {{else if (this.hasHintAndLabelBlocks (has-block 'label') (has-block 'hint')) + }} + <Form::Fields::RadioGroup + @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> + <:default as |group|> + {{yield (hash RadioField=group.RadioField)}} + </:default> + </Form::Fields::RadioGroup> + {{else if (this.hasLabelArgAndHintBlock @label (has-block 'hint'))}} + <Form::Fields::RadioGroup + @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> + <:default as |group|> + {{yield (hash RadioField=group.RadioField)}} + </:default> + </Form::Fields::RadioGroup> + {{else}} + {{! Argument-only case }} + <Form::Fields::RadioGroup + @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 + as |group| + > + {{yield (hash RadioField=group.RadioField)}} + </Form::Fields::RadioGroup> + {{/if}} </@form.Field> \ No newline at end of file diff --git a/packages/ember-toucan-form/src/-private/radio-group-field.ts b/packages/ember-toucan-form/src/-private/radio-group-field.ts index 60ba5ecbf..e82d9143e 100644 --- a/packages/ember-toucan-form/src/-private/radio-group-field.ts +++ b/packages/ember-toucan-form/src/-private/radio-group-field.ts @@ -25,19 +25,20 @@ export interface ToucanFormRadioGroupFieldComponentSignature< */ form: HeadlessFormBlock<DATA>; }; - Blocks: { - default: [ - { - RadioField: BaseRadioGroupFieldSignature['Blocks']['default'][0]['RadioField']; - } - ]; - }; + Blocks: BaseRadioGroupFieldSignature['Blocks']; } export default class ToucanFormTextareaFieldComponent< DATA extends UserData, KEY extends FormKey<FormData<DATA>> = FormKey<FormData<DATA>> > extends Component<ToucanFormRadioGroupFieldComponentSignature<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; diff --git a/packages/ember-toucan-form/src/-private/textarea-field.hbs b/packages/ember-toucan-form/src/-private/textarea-field.hbs index 8858a87a6..25544ae83 100644 --- a/packages/ember-toucan-form/src/-private/textarea-field.hbs +++ b/packages/ember-toucan-form/src/-private/textarea-field.hbs @@ -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> \ No newline at end of file diff --git a/packages/ember-toucan-form/src/-private/textarea-field.ts b/packages/ember-toucan-form/src/-private/textarea-field.ts index ce94d8139..a9164e7ae 100644 --- a/packages/ember-toucan-form/src/-private/textarea-field.ts +++ b/packages/ember-toucan-form/src/-private/textarea-field.ts @@ -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; diff --git a/test-app/tests/integration/components/toucan-form/form-checkbox-group-test.gts b/test-app/tests/integration/components/toucan-form/form-checkbox-group-test.gts index dc715abe8..9825141d7 100644 --- a/test-app/tests/integration/components/toucan-form/form-checkbox-group-test.gts +++ b/test-app/tests/integration/components/toucan-form/form-checkbox-group-test.gts @@ -84,5 +84,154 @@ module( assert.dom('[data-checkbox-group-2]').hasAttribute('readonly'); assert.dom('[data-checkbox-group-3]').hasNoAttribute('readonly'); }); + + test('it renders `@label` and `@hint` component arguments', async function (assert) { + const data: TestData = { + checkboxes: [], + }; + + await render(<template> + <ToucanForm @data={{data}} as |form|> + <form.CheckboxGroup + @label="Label" + @hint="Hint" + @name="checkboxes" + as |group| + > + <group.CheckboxField + @label="Option 1" + @value="option-1" + data-checkbox-group-1 + /> + <group.CheckboxField + @label="Option 2" + @value="option-2" + @isReadOnly={{true}} + data-checkbox-group-2 + /> + <group.CheckboxField + @label="Option 3" + @value="option-3" + data-checkbox-group-3 + /> + </form.CheckboxGroup> + </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 = { + checkboxes: [], + }; + + await render(<template> + <ToucanForm @data={{data}} as |form|> + <form.CheckboxGroup @hint="Hint" @name="checkboxes"> + <:label><span data-label-block>Label</span></:label> + <:default as |group|> + <group.CheckboxField + @label="Option 1" + @value="option-1" + data-checkbox-group-1 + /> + <group.CheckboxField + @label="Option 2" + @value="option-2" + @isReadOnly={{true}} + data-checkbox-group-2 + /> + <group.CheckboxField + @label="Option 3" + @value="option-3" + data-checkbox-group-3 + /> + </:default> + </form.CheckboxGroup> + </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 = { + checkboxes: [], + }; + + await render(<template> + <ToucanForm @data={{data}} as |form|> + <form.CheckboxGroup @label="Label" @name="checkboxes"> + <:hint><span data-hint-block>Hint</span></:hint> + <:default as |group|> + <group.CheckboxField + @label="Option 1" + @value="option-1" + data-checkbox-group-1 + /> + <group.CheckboxField + @label="Option 2" + @value="option-2" + @isReadOnly={{true}} + data-checkbox-group-2 + /> + <group.CheckboxField + @label="Option 3" + @value="option-3" + data-checkbox-group-3 + /> + </:default> + </form.CheckboxGroup> + </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 = { + checkboxes: [], + }; + + await render(<template> + <ToucanForm @data={{data}} as |form|> + <form.CheckboxGroup @name="checkboxes"> + <:label><span data-label-block>Label</span></:label> + <:hint><span data-hint-block>Hint</span></:hint> + <:default as |group|> + <group.CheckboxField + @label="Option 1" + @value="option-1" + data-checkbox-group-1 + /> + <group.CheckboxField + @label="Option 2" + @value="option-2" + @isReadOnly={{true}} + data-checkbox-group-2 + /> + <group.CheckboxField + @label="Option 3" + @value="option-3" + data-checkbox-group-3 + /> + </:default> + </form.CheckboxGroup> + </ToucanForm> + </template>); + + assert.dom('[data-label-block]').exists(); + assert.dom('[data-hint-block]').exists(); + }); } ); diff --git a/test-app/tests/integration/components/toucan-form/form-checkbox-test.gts b/test-app/tests/integration/components/toucan-form/form-checkbox-test.gts index 3ff77ff95..acf11e984 100644 --- a/test-app/tests/integration/components/toucan-form/form-checkbox-test.gts +++ b/test-app/tests/integration/components/toucan-form/form-checkbox-test.gts @@ -31,4 +31,77 @@ module('Integration | Component | ToucanForm | Checkbox', function (hooks) { assert.dom('[data-checkbox]').hasAttribute('readonly'); }); + + test('it renders `@label` and `@hint` component arguments', async function (assert) { + const data: TestData = { + checked: false, + }; + + await render(<template> + <ToucanForm @data={{data}} as |form|> + <form.Checkbox @label="Label" @hint="Hint" @name="checked" /> + </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 = { + checked: false, + }; + + await render(<template> + <ToucanForm @data={{data}} as |form|> + <form.Checkbox @name="checked" @hint="Hint"> + <:label><span data-label-block>Label</span></:label> + </form.Checkbox> + </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 = { + checked: false, + }; + + await render(<template> + <ToucanForm @data={{data}} as |form|> + <form.Checkbox @label="Label" @name="checked"> + <:hint><span data-hint-block>Hint</span></:hint> + </form.Checkbox> + </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 = { + checked: false, + }; + + await render(<template> + <ToucanForm @data={{data}} as |form|> + <form.Checkbox @label="Label" @name="checked"> + <:label><span data-label-block>Label</span></:label> + <:hint><span data-hint-block>Hint</span></:hint> + </form.Checkbox> + </ToucanForm> + </template>); + + assert.dom('[data-label-block]').exists(); + assert.dom('[data-hint-block]').exists(); + }); }); diff --git a/test-app/tests/integration/components/toucan-form/form-input-test.gts b/test-app/tests/integration/components/toucan-form/form-input-test.gts index 0d58a69e5..31ba4d52f 100644 --- a/test-app/tests/integration/components/toucan-form/form-input-test.gts +++ b/test-app/tests/integration/components/toucan-form/form-input-test.gts @@ -31,4 +31,77 @@ module('Integration | Component | ToucanForm | Input', function (hooks) { assert.dom('[data-input]').hasAttribute('readonly'); }); + + test('it renders `@label` and `@hint` component arguments', async function (assert) { + const data: TestData = { + text: 'text', + }; + + await render(<template> + <ToucanForm @data={{data}} as |form|> + <form.Input @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: 'text', + }; + + await render(<template> + <ToucanForm @data={{data}} as |form|> + <form.Input @name="text" @hint="Hint"> + <:label><span data-label-block>Label</span></:label> + </form.Input> + </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: 'text', + }; + + await render(<template> + <ToucanForm @data={{data}} as |form|> + <form.Input @label="Label" @name="text"> + <:hint><span data-hint-block>Hint</span></:hint> + </form.Input> + </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: 'text', + }; + + await render(<template> + <ToucanForm @data={{data}} as |form|> + <form.Input @label="Label" @name="text"> + <:label><span data-label-block>Label</span></:label> + <:hint><span data-hint-block>Hint</span></:hint> + </form.Input> + </ToucanForm> + </template>); + + assert.dom('[data-label-block]').exists(); + assert.dom('[data-hint-block]').exists(); + }); }); diff --git a/test-app/tests/integration/components/toucan-form/form-radio-group-test.gts b/test-app/tests/integration/components/toucan-form/form-radio-group-test.gts index 44a1ac7d8..8af891945 100644 --- a/test-app/tests/integration/components/toucan-form/form-radio-group-test.gts +++ b/test-app/tests/integration/components/toucan-form/form-radio-group-test.gts @@ -58,4 +58,124 @@ module('Integration | Component | ToucanForm | RadioGroup', function (hooks) { assert.dom('[data-radio-1]').hasNoAttribute('readonly'); assert.dom('[data-radio-2]').hasAttribute('readonly'); }); + + test('it renders `@label` and `@hint` component arguments', async function (assert) { + const data: TestData = { + radio: 'option-2', + }; + + await render(<template> + <ToucanForm @data={{data}} as |form|> + <form.RadioGroup @label="Label" @hint="Hint" @name="radio" as |group|> + <group.RadioField @label="option-1" @value="option-1" data-radio-1 /> + <group.RadioField + @label="option-2" + @value="option-2" + @isReadOnly={{true}} + data-radio-2 + /> + </form.RadioGroup> + </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 = { + radio: 'option-2', + }; + + await render(<template> + <ToucanForm @data={{data}} as |form|> + <form.RadioGroup @hint="Hint" @name="radio"> + <:label><span data-label-block>Label</span></:label> + <:default as |group|> + <group.RadioField + @label="option-1" + @value="option-1" + data-radio-1 + /> + <group.RadioField + @label="option-2" + @value="option-2" + @isReadOnly={{true}} + data-radio-2 + /> + </:default> + </form.RadioGroup> + </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 = { + radio: 'option-2', + }; + + await render(<template> + <ToucanForm @data={{data}} as |form|> + <form.RadioGroup @label="Label" @name="radio"> + <:hint><span data-hint-block>Hint</span></:hint> + <:default as |group|> + <group.RadioField + @label="option-1" + @value="option-1" + data-radio-1 + /> + <group.RadioField + @label="option-2" + @value="option-2" + @isReadOnly={{true}} + data-radio-2 + /> + </:default> + </form.RadioGroup> + </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 = { + radio: 'option-2', + }; + + await render(<template> + <ToucanForm @data={{data}} as |form|> + <form.RadioGroup @name="radio"> + <:label><span data-label-block>Label</span></:label> + <:hint><span data-hint-block>Hint</span></:hint> + <:default as |group|> + <group.RadioField + @label="option-1" + @value="option-1" + data-radio-1 + /> + <group.RadioField + @label="option-2" + @value="option-2" + @isReadOnly={{true}} + data-radio-2 + /> + </:default> + </form.RadioGroup> + </ToucanForm> + </template>); + + assert.dom('[data-label-block]').exists(); + assert.dom('[data-hint-block]').exists(); + }); }); diff --git a/test-app/tests/integration/components/toucan-form/form-textarea-test.gts b/test-app/tests/integration/components/toucan-form/form-textarea-test.gts index 008859758..b8e50ea93 100644 --- a/test-app/tests/integration/components/toucan-form/form-textarea-test.gts +++ b/test-app/tests/integration/components/toucan-form/form-textarea-test.gts @@ -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(); + }); });