diff --git a/addon/components/validated-button.hbs b/addon/components/validated-button.hbs index 1cea0831..9a49a78d 100644 --- a/addon/components/validated-button.hbs +++ b/addon/components/validated-button.hbs @@ -1,9 +1,9 @@ {{#let (component this.buttonComponent - onClick=@action - loading=@loading - disabled=@disabled + onClick=this.click + loading=this.loading + disabled=(or @disabled this.loading) label=@label type=@type ) diff --git a/addon/components/validated-button.js b/addon/components/validated-button.js index 5fe66637..4bb4dbf3 100644 --- a/addon/components/validated-button.js +++ b/addon/components/validated-button.js @@ -1,7 +1,55 @@ +import { action } from "@ember/object"; import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { resolve } from "rsvp"; import themedComponent from "../-private/themed-component"; +const ON_CLICK = "on-click"; +const ON_INVALID_CLICK = "on-invalid-click"; export default class ValidatedButtonComponent extends Component { @themedComponent("validated-button/button") buttonComponent; + + @tracked _loading; + + get loading() { + return this.args.loading || this._loading; + } + + @action + async click(event) { + // handle only clicks for custom buttons + // everything else is handled by the validated form itself + if (this.args.type !== "button") { + return this.args.action(event); + } + + event.preventDefault(); + const model = this.args.model; + + if (!model || !model.validate) { + this.runCallback(ON_CLICK); + return; + } + + await model.validate(); + + if (model.get("isInvalid")) { + this.runCallback(ON_INVALID_CLICK); + } else { + this.runCallback(ON_CLICK); + } + } + + runCallback(callbackProp) { + const callback = this.args[callbackProp]; + if (typeof callback !== "function") { + return; + } + + this._loading = true; + resolve(callback(this.args.model)).finally(() => { + this._loading = false; + }); + } } diff --git a/addon/components/validated-form.hbs b/addon/components/validated-form.hbs index 14eb9ae8..407841d9 100644 --- a/addon/components/validated-form.hbs +++ b/addon/components/validated-form.hbs @@ -16,6 +16,13 @@ label="Save" action=this.submit ) + button=(component + "validated-button" + type="button" + loading=this.loading + label="Action" + model=@model + ) ) }} \ No newline at end of file diff --git a/addon/components/validated-input/types/checkbox-group.js b/addon/components/validated-input/types/checkbox-group.js index 8422287f..2ed135e1 100644 --- a/addon/components/validated-input/types/checkbox-group.js +++ b/addon/components/validated-input/types/checkbox-group.js @@ -3,7 +3,7 @@ import Component from "@glimmer/component"; export default class CheckboxGroupComponent extends Component { @action - onUpdate(event, key) { + onUpdate(key, event) { event.preventDefault(); const value = this.value || []; diff --git a/tests/dummy/app/templates/docs/components/validated-button.md b/tests/dummy/app/templates/docs/components/validated-button.md index 05490417..54c1aea4 100644 --- a/tests/dummy/app/templates/docs/components/validated-button.md +++ b/tests/dummy/app/templates/docs/components/validated-button.md @@ -1,20 +1,25 @@ # Validated button -`{{validated-form}}` also yields a submit button component that can be -accessed with `{{f.submit}}`. You can also use it as a block style component -`{{#f.submit}}Test{{/f.submit}}` if you don't want to pass the label as a -property. It takes the following properties: +`{{validated-form}}` yields two kinds of button components: +- `{{f.submit}}`: a submit button for the form +- `{{f.button}}`: a customizable button without HTML-form specific functionality. -**label ``** +You can use them as a block style component `{{#f.submit}}Test{{/f.submit}}` if you don't want to pass the label as a +property. + +Both take the following properties: + +**label ``** The label of the form button. -**type ``** -Type of the button. Default: `button`. +**type ``** +Type of the button. Default for submit: `submit` and for standard button: `button`. +*Watch out:* If you define `type=submit` then the `on-submit` handler of the form will be triggered. -**disabled ``** +**disabled ``** Specifies if the button is disabled. -**loading ``** +**loading ``** Specifies if the button is loading. Default: Automatic integration of `ember-concurrency`. @@ -30,3 +35,28 @@ Specifies if the button is loading. Default: Automatic integration of `ember-con {{demo.snippet 'button-template.hbs'}} {{/docs-demo}} + + +Further you can leverage the `{{f.button}}` component for custom actions. The model of the wrapping form component will get passed to the on-click handler as first argument. + +Passing a custom on click function is possible on the `{{f.buttton}}` via: + +**on-click ``** +Passes an on-click function to the button component. + +**on-invalid-click ``** +Passes a function which is triggered after clicking on the button and when the validation proved the contents to be invalid. + + +{{#docs-demo as |demo|}} + {{#demo.example name='button-advanced-template.hbs'}} + {{#validated-form as |f|}} + {{f.button label='Custom action' on-click=(action (mut triggered) true)}} + {{#f.button on-click=(action (mut triggered) true)}}Custom action button in block style...{{/f.button}} + {{if triggered 'Action triggered!'}} + {{/validated-form}} + {{/demo.example}} + + {{demo.snippet 'button-advanced-template.hbs'}} +{{/docs-demo}} + \ No newline at end of file diff --git a/tests/integration/components/validated-form-test.js b/tests/integration/components/validated-form-test.js index e76462d4..aa867599 100644 --- a/tests/integration/components/validated-form-test.js +++ b/tests/integration/components/validated-form-test.js @@ -10,17 +10,11 @@ import { defer } from "rsvp"; module("Integration | Component | validated form", function (hooks) { setupRenderingTest(hooks); - hooks.beforeEach(function () { - this.actions = {}; - this.send = (actionName, ...args) => - this.actions[actionName].apply(this, args); - }); - test("it renders simple inputs", async function (assert) { await render(hbs` - {{#validated-form as |f|}} - {{f.input label="First name"}} - {{/validated-form}} + + + `); assert.dom("form label").hasText("First name"); @@ -29,9 +23,9 @@ module("Integration | Component | validated form", function (hooks) { test("it renders textareas", async function (assert) { await render(hbs` - {{#validated-form as |f|}} - {{f.input type="textarea" label="my label"}} - {{/validated-form}} + + + `); assert.dom("form label").hasText("my label"); @@ -48,9 +42,9 @@ module("Integration | Component | validated form", function (hooks) { }); await render(hbs` - {{#validated-form as |f|}} - {{f.input type='radioGroup' label='Options' name='testOptions' options=buttonGroupData.options}} - {{/validated-form}} + + + `); assert.dom('input[type="radio"]').exists({ count: 3 }); @@ -65,15 +59,15 @@ module("Integration | Component | validated form", function (hooks) { }); test("it renders submit buttons", async function (assert) { - this.actions.stub = function () {}; + this.set("stub", function () {}); await render(hbs` - {{#validated-form - on-submit=(action "stub") - as |f|}} - {{f.input label="First name"}} - {{f.submit label="Save!"}} - {{/validated-form}} + + + + `); assert.dom("form button").hasAttribute("type", "submit"); @@ -82,9 +76,9 @@ module("Integration | Component | validated form", function (hooks) { test("it renders an always-showing hint", async function (assert) { await render(hbs` - {{#validated-form as |f|}} - {{f.input label="First name" hint="Not your middle name!"}} - {{/validated-form}} + + + `); assert.dom("input + div").doesNotExist(); @@ -94,30 +88,30 @@ module("Integration | Component | validated form", function (hooks) { test("does not render a

tag for buttons if no callbacks were passed", async function (assert) { await render(hbs` - {{#validated-form as |f|}} - {{f.input label="First name"}} - {{/validated-form}} + + + `); assert.dom("form > p").doesNotExist(); }); test("it supports default button labels", async function (assert) { - this.actions.stub = function () {}; + this.set("stub", function () {}); await render(hbs` - {{#validated-form - on-submit=(action "stub") - as |f|}} - {{f.submit}} - {{/validated-form}} + + + `); assert.dom("form button[type=submit]").hasText("Save"); }); test("it performs basic validations on submit", async function (assert) { - this.actions.submit = function () {}; + this.set("submit", function () {}); this.set("UserValidations", UserValidations); run(() => { @@ -130,13 +124,13 @@ module("Integration | Component | validated form", function (hooks) { }); await render(hbs` - {{#validated-form - model=(changeset model UserValidations) - on-submit=(action "submit") - as |f|}} - {{f.input label="First name" name="firstName"}} - {{f.submit}} - {{/validated-form}} + + + + `); assert.dom("span.invalid-feedback").doesNotExist(); @@ -152,9 +146,9 @@ module("Integration | Component | validated form", function (hooks) { test("it calls on-invalid-submit after submit if changeset is invalid", async function (assert) { let invalidSubmitCalled; - this.actions.invalidSubmit = function () { + this.set("invalidSubmit", function () { invalidSubmitCalled = true; - }; + }); this.set("UserValidations", UserValidations); run(() => { @@ -167,13 +161,14 @@ module("Integration | Component | validated form", function (hooks) { }); await render(hbs` - {{#validated-form - model=(changeset model UserValidations) - on-invalid-submit=(action "invalidSubmit") - as |f|}} - {{f.input label="First name" name="firstName"}} - {{f.submit}} - {{/validated-form}} + + + + `); await click("button"); @@ -183,26 +178,26 @@ module("Integration | Component | validated form", function (hooks) { test("it does not call on-invalid-submit after submit if changeset is valid", async function (assert) { let invalidSubmitCalled, submitCalled; - this.actions.submit = function () { + this.set("submit", function () { submitCalled = true; - }; - this.actions.invalidSubmit = function () { + }); + this.set("invalidSubmit", function () { invalidSubmitCalled = true; - }; + }); run(() => { this.set("model", EmberObject.create({})); }); await render(hbs` - {{#validated-form - model=model - on-submit=(action "submit") - on-invalid-submit=(action "invalidSubmit") - as |f|}} - {{f.input label="First name" name="firstName"}} - {{f.submit}} - {{/validated-form}} + + + + `); await click("button"); @@ -211,8 +206,79 @@ module("Integration | Component | validated form", function (hooks) { assert.true(submitCalled); }); + test("it performs validation and calls onClick function on custom buttons", async function (assert) { + assert.expect(3); + + this.set("onClick", function (model) { + assert.step("onClick"); + assert.strictEqual(model.firstName, "xenia"); + }); + this.set("onInvalidClick", function () { + assert.step("onInvalidClick"); + }); + + run(() => { + this.set( + "model", + EmberObject.create({ + firstName: "xenia", + }) + ); + }); + + await render(hbs` + + + + + `); + + await click("button"); + + assert.verifySteps(["onClick"]); + }); + + test("it performs validation and calls onInvalidClick function on custom buttons", async function (assert) { + assert.expect(3); + + this.set("onClick", function () { + assert.step("onClick"); + }); + this.set("onInvalidClick", function (model) { + assert.step("onInvalidClick"); + assert.strictEqual(model.firstName, "x"); + }); + this.set("UserValidations", UserValidations); + + run(() => { + this.set( + "model", + EmberObject.create({ + firstName: "x", + }) + ); + }); + + await render(hbs` + + + + + `); + + await click("button"); + + assert.verifySteps(["onInvalidClick"]); + }); + test("it performs basic validations on focus out", async function (assert) { - this.actions.submit = function () {}; + this.set("submit", function () {}); this.set("UserValidations", UserValidations); run(() => { @@ -220,12 +286,14 @@ module("Integration | Component | validated form", function (hooks) { }); await render(hbs` - {{#validated-form - model=(changeset model UserValidations) - on-submit=(action "submit") - as |f|}} - {{f.input label="First name" name="firstName"}} - {{/validated-form}} + + + + `); assert.dom("input + div").doesNotExist(); @@ -238,7 +306,7 @@ module("Integration | Component | validated form", function (hooks) { }); test("it skips basic validations on focus out with validateBeforeSubmit=false set on the form", async function (assert) { - this.actions.submit = function () {}; + this.set("submit", function () {}); this.set("UserValidations", UserValidations); run(() => { @@ -246,14 +314,14 @@ module("Integration | Component | validated form", function (hooks) { }); await render(hbs` - {{#validated-form - model=(changeset model UserValidations) - on-submit=(action "submit") - validateBeforeSubmit=false - as |f|}} - {{f.input label="First name" name="firstName"}} - {{f.submit}} - {{/validated-form}} + + + + `); assert.dom("span.invalid-feedback").doesNotExist(); @@ -269,7 +337,7 @@ module("Integration | Component | validated form", function (hooks) { }); test("it skips basic validations on focus out with validateBeforeSubmit=false set on the input", async function (assert) { - this.actions.submit = function () {}; + this.set("submit", function () {}); this.set("UserValidations", UserValidations); run(() => { @@ -277,12 +345,12 @@ module("Integration | Component | validated form", function (hooks) { }); await render(hbs` - {{#validated-form - model=(changeset model UserValidations) - on-submit=(action "submit") - as |f|}} - {{f.input label="First name" name="firstName" validateBeforeSubmit=false}} - {{/validated-form}} + + + `); assert.dom("input + div").doesNotExist(); @@ -296,19 +364,19 @@ module("Integration | Component | validated form", function (hooks) { test("on-submit can be an action returning a promise", async function (assert) { const deferred = defer(); - this.actions.submit = () => deferred.promise; + this.set("submit", () => deferred.promise); run(() => { this.set("model", EmberObject.create({})); }); await render(hbs` - {{#validated-form - model=(changeset model) - on-submit=(action "submit") - as |f|}} - {{f.submit class=(if f.loading 'loading')}} - {{/validated-form}} + + + `); assert.dom("button").doesNotHaveClass("loading"); @@ -323,19 +391,19 @@ module("Integration | Component | validated form", function (hooks) { }); test("on-submit can be an action returning a non-promise", async function (assert) { - this.actions.submit = () => undefined; + this.set("submit", () => undefined); run(() => { this.set("model", EmberObject.create({})); }); await render(hbs` - {{#validated-form - model=(changeset model) - on-submit=(action "submit") - as |f|}} - {{f.submit}} - {{/validated-form}} + + + `); assert.dom("button").doesNotHaveClass("loading"); @@ -348,22 +416,22 @@ module("Integration | Component | validated form", function (hooks) { test("it yields the loading state", async function (assert) { const deferred = defer(); - this.actions.submit = () => deferred.promise; + this.set("submit", () => deferred.promise); run(() => { this.set("model", EmberObject.create({})); }); await render(hbs` - {{#validated-form - model=(changeset model) - on-submit=(action "submit") - as |f|}} + {{#if f.loading}} loading... {{/if}} - {{f.submit}} - {{/validated-form}} + + `); assert.dom("span.loading").doesNotExist(); @@ -379,9 +447,9 @@ module("Integration | Component | validated form", function (hooks) { test("it handles being removed from the DOM during sync submit", async function (assert) { this.set("show", true); - this.actions.submit = () => { + this.set("submit", () => { this.set("show", false); - }; + }); run(() => { this.set("model", EmberObject.create({})); @@ -389,15 +457,15 @@ module("Integration | Component | validated form", function (hooks) { await render(hbs` {{#if show}} - {{#validated-form - model=(changeset model) - on-submit=(action "submit") - as |f|}} + {{#if f.loading}} loading... {{/if}} - {{f.submit}} - {{/validated-form}} + + {{/if}} `); @@ -409,11 +477,11 @@ module("Integration | Component | validated form", function (hooks) { this.set("show", true); const deferred = defer(); - this.actions.submit = () => { + this.set("submit", () => { return deferred.promise.then(() => { this.set("show", false); }); - }; + }); run(() => { this.set("model", EmberObject.create({})); @@ -421,15 +489,15 @@ module("Integration | Component | validated form", function (hooks) { await render(hbs` {{#if show}} - {{#validated-form - model=(changeset model) - on-submit=(action "submit") - as |f|}} + {{#if f.loading}} loading... {{/if}} - {{f.submit}} - {{/validated-form}} + + {{/if}} `); @@ -440,8 +508,8 @@ module("Integration | Component | validated form", function (hooks) { test("it binds the autocomplete attribute", async function (assert) { await render(hbs` - {{#validated-form autocomplete="off"}} - {{/validated-form}} + + `); assert.dom("form").hasAttribute("autocomplete", "off");