From 32dc8bb9a834ffb08c3835fd63f90f18fa92ad22 Mon Sep 17 00:00:00 2001 From: Tyler Senter Date: Mon, 26 Aug 2024 09:06:33 -0400 Subject: [PATCH] feat: Implement checkbox component (#100) * feat: Implement checkbox component * tests: Add tests for checkbox component * docs: Add route for Checkbox component * feat(field): Don't render label if not provided * chore: PR review * chore: Update phone-field description --- .../app/components/f/form/checkbox.gts | 117 ++++++++++++++++++ .../app/components/f/form/phone-field.gts | 2 +- apps/ember-test-app/app/router.ts | 1 + .../app/templates/components.hbs | 1 + .../templates/components/form/checkbox.hbs | 5 + .../components/form/checkbox-test.gts | 104 ++++++++++++++++ packages/ember/package.json | 1 + .../ember/src/components/form/checkbox.gts | 82 ++++++++++++ packages/ember/src/components/form/field.gts | 36 ++++-- 9 files changed, 337 insertions(+), 12 deletions(-) create mode 100644 apps/ember-test-app/app/components/f/form/checkbox.gts create mode 100644 apps/ember-test-app/app/templates/components/form/checkbox.hbs create mode 100644 apps/ember-test-app/tests/integration/components/form/checkbox-test.gts create mode 100644 packages/ember/src/components/form/checkbox.gts diff --git a/apps/ember-test-app/app/components/f/form/checkbox.gts b/apps/ember-test-app/app/components/f/form/checkbox.gts new file mode 100644 index 000000000..ce37feff4 --- /dev/null +++ b/apps/ember-test-app/app/components/f/form/checkbox.gts @@ -0,0 +1,117 @@ +import { array, fn } from '@ember/helper'; +import { action, set } from '@ember/object'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import Checkbox from '@nrg-ui/ember/components/form/checkbox'; +import bind from '@nrg-ui/ember/helpers/bind'; +import FreestyleUsage from 'ember-freestyle/components/freestyle/usage'; +import FreestyleSection from 'ember-freestyle/components/freestyle-section'; + +import CodeBlock from '../../code-block'; + +// TypeScript doesn't recognize that this function is used in the template +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function log(...msg: string[]) { + console.log(msg.join(' ')); +} + +class Model { + @tracked + property = ''; +} + +export default class extends Component { + model = new Model(); + + @tracked + class = ''; + + @tracked + disabled = false; + + @tracked + inline; + + @tracked + label = 'Checkbox label'; + + @tracked + type = 'checkbox'; + + @action + update(key: string, value: unknown) { + set(this, key, value); + } + + +} diff --git a/apps/ember-test-app/app/components/f/form/phone-field.gts b/apps/ember-test-app/app/components/f/form/phone-field.gts index 0dc716982..d15c23a2c 100644 --- a/apps/ember-test-app/app/components/f/form/phone-field.gts +++ b/apps/ember-test-app/app/components/f/form/phone-field.gts @@ -60,7 +60,7 @@ export default class extends Component { <:api as |Args|> <:group as |Item|> + diff --git a/apps/ember-test-app/app/templates/components/form/checkbox.hbs b/apps/ember-test-app/app/templates/components/form/checkbox.hbs new file mode 100644 index 000000000..bf0c86dd1 --- /dev/null +++ b/apps/ember-test-app/app/templates/components/form/checkbox.hbs @@ -0,0 +1,5 @@ +{{page-title "Checkbox"}} + +
+ +
\ No newline at end of file diff --git a/apps/ember-test-app/tests/integration/components/form/checkbox-test.gts b/apps/ember-test-app/tests/integration/components/form/checkbox-test.gts new file mode 100644 index 000000000..bc03681f1 --- /dev/null +++ b/apps/ember-test-app/tests/integration/components/form/checkbox-test.gts @@ -0,0 +1,104 @@ +import { click, render, settled } from '@ember/test-helpers'; +import { tracked } from '@glimmer/tracking'; +import Checkbox from '@nrg-ui/ember/components/form/checkbox'; +import bind from '@nrg-ui/ember/helpers/bind'; +import { setupRenderingTest } from 'ember-test-app/tests/helpers'; +import { module, test } from 'qunit'; + +module('Integration | Component | form/checkbox', function (hooks) { + setupRenderingTest(hooks); + + class Model { + @tracked + value: boolean = false; + } + + test('it renders', async function (assert) { + const model = new Model(); + await render(); + + assert + .dom('.form-check > input') + .hasAttribute('role', 'checkbox') + .hasAttribute('type', 'checkbox') + .hasClass('form-check-input') + .hasValue('false') + .isNotChecked(); + + const labelId = this.element.querySelector('.form-check > input').id; + + assert + .dom('.form-check > label') + .hasAttribute('for', labelId) + .hasText('This is a checkbox'); + + model.value = true; + await settled(); + + assert.dom('.form-check > input').hasValue('true').isChecked(); + }); + + test('it renders (switch)', async function (assert) { + const model = new Model(); + await render(); + + assert.dom('div').hasClass('form-switch'); + + assert + .dom('.form-check > input') + .hasAttribute('role', 'switch') + .hasAttribute('type', 'checkbox') + .hasClass('form-check-input') + .hasValue('false') + .isNotChecked(); + + const labelId = this.element.querySelector('.form-check > input').id; + + assert + .dom('.form-check > label') + .hasAttribute('for', labelId) + .hasText('This is a checkbox'); + + model.value = true; + await settled(); + + assert.dom('.form-check > input').hasValue('true').isChecked(); + }); + + test('it works', async function (assert) { + const model = new Model(); + await render(); + + assert.dom('.form-check > input').hasValue('false').isNotChecked(); + + model.value = true; + await settled(); + + assert.dom('.form-check > input').hasValue('true').isChecked(); + + await click('.form-check > input'); + + assert.dom('.form-check > input').hasValue('false').isNotChecked(); + assert.false(model.value); + + await click('.form-check > input'); + + assert.dom('.form-check > input').hasValue('true').isChecked(); + assert.true(model.value); + }); +}); diff --git a/packages/ember/package.json b/packages/ember/package.json index 10b56e892..0d2de7800 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -164,6 +164,7 @@ "./components/card.js": "./dist/_app_/components/card.js", "./components/footer.js": "./dist/_app_/components/footer.js", "./components/form/bound-value.js": "./dist/_app_/components/form/bound-value.js", + "./components/form/checkbox.js": "./dist/_app_/components/form/checkbox.js", "./components/form/field.js": "./dist/_app_/components/form/field.js", "./components/form/index.js": "./dist/_app_/components/form/index.js", "./components/form/phone-field.js": "./dist/_app_/components/form/phone-field.js", diff --git a/packages/ember/src/components/form/checkbox.gts b/packages/ember/src/components/form/checkbox.gts new file mode 100644 index 000000000..129f36266 --- /dev/null +++ b/packages/ember/src/components/form/checkbox.gts @@ -0,0 +1,82 @@ +import { on } from '@ember/modifier'; +import { action } from '@ember/object'; + +import BoundValue from './bound-value.ts'; + +export interface CheckboxSignature { + Element: HTMLInputElement; + Args: { + describedBy?: string; + disabled?: boolean; + id?: string; + inline?: boolean; + isInvalid?: boolean; + isWarning?: boolean; + label?: string; + type?: 'checkbox' | 'switch'; + }; + Blocks: { + default: []; + }; +} + +export default class FormCheckbox extends BoundValue< + CheckboxSignature, + boolean +> { + get classList() { + const classes = ['form-check-input']; + + if (this.args.isInvalid) { + classes.push('is-invalid'); + } else if (this.args.isWarning) { + classes.push('is-warning'); + } + + return classes.join(' '); + } + + get divClassList() { + const classList = ['form-check']; + + if (this.isSwitch) { + classList.push('form-switch'); + } + + if (this.args.inline) { + classList.push('form-check-inline'); + } + + return classList.join(' '); + } + + get isSwitch() { + return this.args.type === 'switch'; + } + + @action + change(evt: Event) { + const target = evt.target as HTMLInputElement; + this.onChange(target.checked); + } + + +} diff --git a/packages/ember/src/components/form/field.gts b/packages/ember/src/components/form/field.gts index fe05549cc..fabf720da 100644 --- a/packages/ember/src/components/form/field.gts +++ b/packages/ember/src/components/form/field.gts @@ -5,6 +5,7 @@ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { runTask } from 'ember-lifeline'; +import Checkbox from './checkbox.gts'; import PhoneField from './phone-field.gts'; import RadioGroup from './radio-group.gts'; import Select from './select.gts'; @@ -13,6 +14,7 @@ import TextField from './text-field.gts'; import onUpdate from '../../modifiers/on-update.ts'; import { PresenceValidator } from '../../validation/index.ts'; +import type { CheckboxSignature } from './checkbox.gts'; import type { FormType } from './index.gts'; import type { RadioGroupFieldSignature } from './radio-group.gts'; import type { SelectSignature } from './select.gts'; @@ -44,6 +46,7 @@ export interface FieldSignature { Blocks: { default: [ { + Checkbox: ComponentLike; Phone: ComponentLike; RadioGroup: ComponentLike; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -198,19 +201,30 @@ export default class Field extends Component { }