From f171980ad16870585f1ec06dfcd4d02e4be927c9 Mon Sep 17 00:00:00 2001 From: Falk Date: Tue, 8 Mar 2022 16:24:25 +0100 Subject: [PATCH] feat(emeisOptions): passing options as a function - this enable to pass options for meta-fields of type choice as static list or as a function --- README.md | 12 +- addon/components/meta-field.hbs | 32 +++ addon/components/meta-field.js | 50 ++++ addon/components/meta-fields.hbs | 30 --- addon/components/meta-fields.js | 14 -- addon/helpers/eval-meta.js | 8 - addon/templates/scopes/edit.hbs | 8 +- addon/templates/users/edit.hbs | 7 +- app/components/meta-field.js | 1 + app/components/meta-fields.js | 1 - app/helpers/eval-meta.js | 1 - tests/dummy/app/services/emeis-options.js | 49 ++-- .../integration/components/meta-field-test.js | 221 ++++++++++++++++++ .../components/meta-fields-test.js | 199 ---------------- tests/integration/helpers/eval-meta-test.js | 41 ---- 15 files changed, 357 insertions(+), 317 deletions(-) create mode 100644 addon/components/meta-field.hbs create mode 100644 addon/components/meta-field.js delete mode 100644 addon/components/meta-fields.hbs delete mode 100644 addon/components/meta-fields.js delete mode 100644 addon/helpers/eval-meta.js create mode 100644 app/components/meta-field.js delete mode 100644 app/components/meta-fields.js delete mode 100644 app/helpers/eval-meta.js create mode 100644 tests/integration/components/meta-field-test.js delete mode 100644 tests/integration/components/meta-fields-test.js delete mode 100644 tests/integration/helpers/eval-meta-test.js diff --git a/README.md b/README.md index ec5de6dd..77a423ff 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,12 @@ export default class EmeisOptionsService extends Service { { slug: "test-input-2", label: "some.translation.key", + options: [ // insert a static list of options (value, label), or a (async) function which resolves to a list of options + { + value: "option-1", + label: "Option one" + } + ], type: "choice", visible: () => true, readOnly: false @@ -134,12 +140,16 @@ export default class EmeisOptionsService extends Service { _Watch out_ - the translation key has to be present in your local translation files. -There are special options available for `type` and `visible` properties. +There are special options available for `options`, `type` and `visible` properties. #### **type** - meta field Defines the type of the output component and can either be a _text_ or a _choice_. +#### **options** - meta field + +In combination with `type:"choice"` the options can be a list of options (`{value, label}`) or a (async) function which resolves to a list of options. + #### **visible** & **readOnly** meta field Accepts a boolean value for static visibility or a function which evaluates to a boolean value. Submitted functions will evaluate live while rendering. diff --git a/addon/components/meta-field.hbs b/addon/components/meta-field.hbs new file mode 100644 index 00000000..7fba2a5e --- /dev/null +++ b/addon/components/meta-field.hbs @@ -0,0 +1,32 @@ +{{#if this.visible}} + + {{#if (eq @field.type "choice")}} + {{#if this.options.isRunning}} + + {{else}} + + {{optional-translate option.label}} + + {{/if}} + {{/if}} + {{#if (eq @field.type "text")}} + + {{/if}} + +{{/if}} \ No newline at end of file diff --git a/addon/components/meta-field.js b/addon/components/meta-field.js new file mode 100644 index 00000000..cbe5f1fa --- /dev/null +++ b/addon/components/meta-field.js @@ -0,0 +1,50 @@ +import { assert } from "@ember/debug"; +import { action } from "@ember/object"; +import { inject as service } from "@ember/service"; +import Component from "@glimmer/component"; +import { task } from "ember-concurrency"; +import { useTask } from "ember-resources"; + +export default class MetaFieldComponent extends Component { + @service intl; + + constructor(...args) { + super(...args); + + assert("Must pass a valid field argument", this.args.field); + assert("Must pass a valid model argument", this.args.model); + } + + evaluateProperty(expression) { + if (typeof expression === "boolean") return expression; + if (typeof expression === "function") return expression(this.args.model); + if (typeof expression === "string") return expression === "true"; + return false; + } + + get visible() { + return this.evaluateProperty(this.args.field.visible); + } + + get disabled() { + return this.evaluateProperty(this.args.field.readOnly); + } + + options = useTask(this, this.fetchOptions, () => [this.args.field.options]); + + @task + *fetchOptions(options) { + // options may be a (asnyc) function or a complex property + if (typeof options !== "function") { + return options; + } + return yield options(); + } + + @action + updateMetaField(field, model, optionOrEvent) { + const value = optionOrEvent?.target?.value ?? optionOrEvent?.value; + model.metainfo = { ...model.metainfo, [field.slug]: value }; + model.notifyPropertyChange("metainfo"); + } +} diff --git a/addon/components/meta-fields.hbs b/addon/components/meta-fields.hbs deleted file mode 100644 index fb4421c8..00000000 --- a/addon/components/meta-fields.hbs +++ /dev/null @@ -1,30 +0,0 @@ -{{#each @fields as |field|}} - {{#if (eval-meta field.visible @model)}} - - {{#if (eq field.type "choice")}} - - {{optional-translate option.label}} - - {{/if}} - {{#if (eq field.type "text")}} - - {{/if}} - - {{/if}} -{{/each}} diff --git a/addon/components/meta-fields.js b/addon/components/meta-fields.js deleted file mode 100644 index a7bba2ea..00000000 --- a/addon/components/meta-fields.js +++ /dev/null @@ -1,14 +0,0 @@ -import { action } from "@ember/object"; -import { inject as service } from "@ember/service"; -import Component from "@glimmer/component"; - -export default class EditFormComponent extends Component { - @service intl; - - @action - updateMetaField(field, model, optionOrEvent) { - const value = optionOrEvent?.target?.value ?? optionOrEvent?.value; - model.metainfo = { ...model.metainfo, [field.slug]: value }; - model.notifyPropertyChange("metainfo"); - } -} diff --git a/addon/helpers/eval-meta.js b/addon/helpers/eval-meta.js deleted file mode 100644 index 4f746d0a..00000000 --- a/addon/helpers/eval-meta.js +++ /dev/null @@ -1,8 +0,0 @@ -import { helper } from "@ember/component/helper"; - -export default helper(function evalMeta([expression, model]) { - if (typeof expression === "boolean") return expression; - if (typeof expression === "function") return expression(model); - if (typeof expression === "string") return expression === "true"; - return false; -}); diff --git a/addon/templates/scopes/edit.hbs b/addon/templates/scopes/edit.hbs index c6c7686f..8c9d8c36 100644 --- a/addon/templates/scopes/edit.hbs +++ b/addon/templates/scopes/edit.hbs @@ -38,10 +38,10 @@ - + {{#each this.metaFields as |field|}} + + {{/each}} +
diff --git a/addon/templates/users/edit.hbs b/addon/templates/users/edit.hbs index f27151ba..210cde0b 100644 --- a/addon/templates/users/edit.hbs +++ b/addon/templates/users/edit.hbs @@ -131,10 +131,9 @@ {{/if}} - + {{#each this.metaFields as |field|}} + + {{/each}} { + await timeout(2000); + return [ + { + value: "Option 1", + label: "emeis.options.meta.scope.options.label-1", // again a ember-intl translation key + }, + { + value: "Option 2", + label: "emeis.options.meta.scope.options.label-2", + }, + { + value: "Option 3", + label: "emeis.options.meta.scope.options.label-3", + }, + ]; + }, visible: () => true, // boolean or function which evaluates to a boolean value readOnly: false, }, diff --git a/tests/integration/components/meta-field-test.js b/tests/integration/components/meta-field-test.js new file mode 100644 index 00000000..2bb1b3e6 --- /dev/null +++ b/tests/integration/components/meta-field-test.js @@ -0,0 +1,221 @@ +import { fillIn, render } from "@ember/test-helpers"; +import { hbs } from "ember-cli-htmlbars"; +import { setupIntl } from "ember-intl/test-support"; +import { selectChoose } from "ember-power-select/test-support"; +import { setupRenderingTest } from "ember-qunit"; +import { module, test } from "qunit"; + +const translations = { + metaExample: "Example for custom choice field", + option1: "Ham", + option2: "Cheese", + metaExample2: "Example for custom text field", + dynamicVisibility: "field with dynamic visibility (visible)", + dynamicVisibility2: "field with dynamic visibility (unvisible)", + dynamicReadOnly: "field with dynamic readOnly state", +}; + +const fields = { + choiceEditable: { + slug: "meta-example", + label: "metaExample", + type: "choice", + options: [ + { + value: "option-1", + label: "option1", + }, + { + value: "Option 2", + label: "option2", + }, + ], + visible: true, + readOnly: false, + }, + textEditable: { + slug: "meta-example-2", + label: "metaExample2", + type: "text", + visible: true, + readOnly: false, + }, + invisibleField: { + slug: "invisible", + label: "invisible", + type: "text", + visible: false, + readOnly: true, + }, + dynamicVisibility1: { + slug: "dynamic-visibility", + label: "dynamicVisibility", + type: "text", + visible: () => true, + readOnly: true, + }, + dynamicVisibility2: { + slug: "dynamic-visibility-2", + label: "dynamicVisibility2", + type: "text", + visible: () => 1 > 2, + readOnly: false, + }, + staticReadOnlyText: { + slug: "static-read-only-text", + label: "staticReadyOnlyText", + type: "text", + visible: true, + readOnly: true, + }, + dynamicReadOnlyText: { + slug: "dynamic-read-only-text", + label: "dynamicReadOnly", + type: "text", + visible: true, + readOnly: (model) => model.name === "readOnly", + }, + dynamicReadOnlyChoice: { + slug: "dynamic-read-only-choice", + label: "dynamicReadOnly", + type: "choice", + options: [ + { + value: "option-1", + label: "option1", + }, + { + value: "Option 2", + label: "option2", + }, + ], + visible: true, + readOnly: (model) => model.name === "readOnly", + }, +}; + +module("Integration | Component | meta-field", function (hooks) { + setupRenderingTest(hooks); + setupIntl(hooks, ["en"], translations); + + hooks.beforeEach(function () { + this.model = { + metainfo: {}, + notifyPropertyChange: () => {}, + }; + this.set("fields", fields); + }); + + test("it renders meta field of type select", async function (assert) { + assert.expect(3); + await render(hbs` + + `); + + assert.dom(".ember-power-select-trigger").exists(); + assert.dom(this.element).containsText(translations.metaExample); + + await selectChoose(".ember-power-select-trigger", "Ham"); + assert.deepEqual(this.model.metainfo, { + "meta-example": "option-1", + }); + }); + + test("it renders meta field of type text", async function (assert) { + assert.expect(3); + + await render(hbs` + + `); + + assert.dom("[data-test-meta-field-text]").exists({ count: 1 }); + assert.dom(this.element).containsText(translations.metaExample2); + + await fillIn("[data-test-meta-field-text]", "My value"); + assert.deepEqual(this.model.metainfo, { + "meta-example-2": "My value", + }); + }); + + test("it does not render invisible meta fields", async function (assert) { + assert.expect(2); + + await render(hbs` + + `); + + assert.dom(".ember-power-select-trigger").doesNotExist(); + assert.dom("[data-test-meta-field-text]").doesNotExist(); + }); + + test("it renders fields with dynamically evaluated visibility", async function (assert) { + assert.expect(2); + + await render(hbs` + + + `); + + assert.dom("[data-test-meta-field-text='dynamic-visibility']").exists(); + assert + .dom("[data-test-meta-field-text='dynamic-visibility-2']") + .doesNotExist(); + }); + + test("it renders statically disabled meta field", async function (assert) { + assert.expect(1); + + await render(hbs` + + `); + + assert + .dom("[data-test-meta-field-text='static-read-only-text']") + .hasAttribute("disabled"); + }); + + test("it renders dynamically disabled meta fields", async function (assert) { + assert.expect(3); + + this.model.name = "readOnly"; + + await render(hbs` + + + `); + + assert.dom(".ember-power-select-trigger").hasAttribute("aria-disabled"); + + assert + .dom("[data-test-meta-field-text='dynamic-read-only-text']") + .exists({ count: 1 }); + + assert + .dom("[data-test-meta-field-text='dynamic-read-only-text']") + .hasAttribute("disabled"); + }); +}); diff --git a/tests/integration/components/meta-fields-test.js b/tests/integration/components/meta-fields-test.js deleted file mode 100644 index 83d24f43..00000000 --- a/tests/integration/components/meta-fields-test.js +++ /dev/null @@ -1,199 +0,0 @@ -import Service from "@ember/service"; -import { fillIn, render } from "@ember/test-helpers"; -import { hbs } from "ember-cli-htmlbars"; -import { setupIntl } from "ember-intl/test-support"; -import { selectChoose } from "ember-power-select/test-support"; -import { setupRenderingTest } from "ember-qunit"; -import { module, test } from "qunit"; - -const translations = { - scope: { - metaExample: "Example for custom choice field", - option1: "Ham", - option2: "Cheese", - metaExample2: "Example for custom text field", - dynamicVisibility: "field with dynamic visibility (visible)", - dynamicVisibility2: "field with dynamic visibility (unvisible)", - dynamicReadOnly: "field with dynamic readOnly state", - }, -}; -class EmeisOptionsStub extends Service { - metaFields = { - scope: [ - { - slug: "meta-example", - label: "scope.metaExample", - type: "choice", - options: [ - { - value: "option-1", - label: "scope.option1", - }, - { - value: "Option 2", - label: "scope.option2", - }, - ], - visible: true, - readOnly: false, - }, - { - slug: "meta-example-2", - label: "scope.metaExample2", - type: "text", - visible: true, - readOnly: false, - }, - { - slug: "dynamic-visibility", - label: "scope.dynamicVisibility", - type: "text", - visible: () => true, - readOnly: true, - }, - { - slug: "dynamic-visibility-2", - label: "scope.dynamicVisibility2", - type: "text", - visible: () => 1 > 2, - readOnly: false, - }, - { - slug: "dynamic-readOnly", - label: "scope.dynamicReadOnly", - type: "text", - visible: (model) => model.name === "readOnly", - readOnly: (model) => model.name === "readOnly", - }, - ], - }; -} - -module("Integration | Component | meta-fields", function (hooks) { - setupRenderingTest(hooks); - setupIntl(hooks, ["en"], translations); - - hooks.beforeEach(function () { - this.owner.register("service:emeisOptions", EmeisOptionsStub); - this.emeisOptions = this.owner.lookup("service:emeisOptions"); - - this.model = { - metainfo: {}, - notifyPropertyChange: () => {}, - }; - }); - - test("it renders", async function (assert) { - await render(hbs``); - - assert.dom(this.element).hasText(""); - }); - - test("it renders meta field of type select and text", async function (assert) { - assert.expect(6); - - await render(hbs` - - `); - - assert.dom(".ember-power-select-trigger").exists(); - assert.dom("[data-test-meta-field-text]").exists({ count: 2 }); - - assert.dom(this.element).containsText(translations.scope.metaExample); - assert.dom(this.element).containsText(translations.scope.metaExample2); - - await selectChoose(".ember-power-select-trigger", "Ham"); - assert.deepEqual(this.model.metainfo, { - "meta-example": "option-1", - }); - - await fillIn("[data-test-meta-field-text]", "My value"); - assert.deepEqual(this.model.metainfo, { - "meta-example": "option-1", - "meta-example-2": "My value", - }); - }); - - test("it does not render invisible meta fields", async function (assert) { - assert.expect(2); - - // Set visibility to `false` for each field - this.emeisOptions.metaFields.scope.forEach( - (field) => (field.visible = false) - ); - - await render(hbs` - - `); - - assert.dom(".ember-power-select-trigger").doesNotExist(); - assert.dom("[data-test-meta-field-text]").doesNotExist(); - }); - - test("it renders fields with dynamically evaluated visibility", async function (assert) { - assert.expect(2); - - await render(hbs` - - `); - - assert.dom("[data-test-meta-field-text='dynamic-visibility']").exists(); - assert - .dom("[data-test-meta-field-text='dynamic-visibility-2']") - .doesNotExist(); - }); - - test("it renders disabled meta fields", async function (assert) { - assert.expect(4); - - // Set fields to read-only - this.emeisOptions.metaFields.scope.forEach( - (field) => (field.readOnly = true) - ); - - await render(hbs` - - `); - - assert.dom(".ember-power-select-trigger").exists(); - assert.dom("[data-test-meta-field-text]").exists({ count: 2 }); - - assert.dom(".ember-power-select-trigger").hasAttribute("aria-disabled"); - assert - .dom("[data-test-meta-field-text='dynamic-visibility']") - .hasAttribute("disabled"); - }); - - test("it renders dynamically disabled meta fields", async function (assert) { - assert.expect(2); - - this.model.name = "readOnly"; - - await render(hbs` - - `); - - assert - .dom("[data-test-meta-field-text='dynamic-readOnly']") - .exists({ count: 1 }); - - assert - .dom("[data-test-meta-field-text='dynamic-readOnly']") - .hasAttribute("disabled"); - }); -}); diff --git a/tests/integration/helpers/eval-meta-test.js b/tests/integration/helpers/eval-meta-test.js deleted file mode 100644 index 7dbfbaf3..00000000 --- a/tests/integration/helpers/eval-meta-test.js +++ /dev/null @@ -1,41 +0,0 @@ -import { render } from "@ember/test-helpers"; -import { hbs } from "ember-cli-htmlbars"; -import { setupRenderingTest } from "ember-qunit"; -import { module, test } from "qunit"; - -module("Integration | Helper | eval-meta", function (hooks) { - setupRenderingTest(hooks); - - test("it evaluates strings", async function (assert) { - this.set("inputValue", "1234"); - - await render(hbs`{{eval-meta this.inputValue}}`); - - assert.strictEqual(this.element.textContent, "false"); - - this.set("inputValue", "true"); - assert.strictEqual(this.element.textContent, "true"); - }); - - test("it evaluates booleans", async function (assert) { - this.set("inputValue", true); - - await render(hbs`{{eval-meta this.inputValue}}`); - - assert.strictEqual(this.element.textContent, "true"); - - this.set("inputValue", false); - assert.strictEqual(this.element.textContent, "false"); - }); - - test("it evaluates functions", async function (assert) { - this.set("inputValue", () => true); - - await render(hbs`{{eval-meta this.inputValue}}`); - - assert.strictEqual(this.element.textContent, "true"); - - this.set("inputValue", () => false); - assert.strictEqual(this.element.textContent, "false"); - }); -});