From 87ab332bce4ab5db34bb2b39b8936cf7c66bb479 Mon Sep 17 00:00:00 2001 From: John Cowen Date: Tue, 31 Mar 2020 18:03:15 +0100 Subject: [PATCH] ui: Add ` and `{{state-matches}}` ember component/helper (#7556) This commit adds 2 ember component/helpers and a service to contain the shared functionality for matching/rendering content dependent on state identifiers. Currently a `service.state` method has been added to easily make manual state objects, but these are built towards using `xstate` to manage UI state in some of our future components. We've added some tests here, and we aren't currently using these components anywhere in this commit. --- ui-v2/app/components/state/README.mdx | 38 +++++++++++++++++++ ui-v2/app/components/state/index.hbs | 3 ++ ui-v2/app/components/state/index.js | 18 +++++++++ ui-v2/app/helpers/state-matches.js | 9 +++++ ui-v2/app/services/state.js | 14 +++++++ .../integration/components/state-test.js | 33 ++++++++++++++++ .../integration/helpers/state-matches-test.js | 32 ++++++++++++++++ ui-v2/tests/unit/services/state-test.js | 27 +++++++++++++ 8 files changed, 174 insertions(+) create mode 100644 ui-v2/app/components/state/README.mdx create mode 100644 ui-v2/app/components/state/index.hbs create mode 100644 ui-v2/app/components/state/index.js create mode 100644 ui-v2/app/helpers/state-matches.js create mode 100644 ui-v2/app/services/state.js create mode 100644 ui-v2/tests/integration/components/state-test.js create mode 100644 ui-v2/tests/integration/helpers/state-matches-test.js create mode 100644 ui-v2/tests/unit/services/state-test.js diff --git a/ui-v2/app/components/state/README.mdx b/ui-v2/app/components/state/README.mdx new file mode 100644 index 000000000..8a59e4df1 --- /dev/null +++ b/ui-v2/app/components/state/README.mdx @@ -0,0 +1,38 @@ +## State + +`Currently Idle` + +`` is a renderless component that eases rendering of different states +from within templates. State objects could be manually made state objects and +xstate state objects. It's very similar to a normal conditional in that if the +state identifier matches the current state, the contents of the component will +be shown. + +### Arguments + +| Argument/Attribute | Type | Default | Description | +| --- | --- | --- | --- | +| `state` | `object` | | An object that implements a `match` method | +| `matches` | `String\|Array` | | A state identifier (or array of state identifiers) to match on | + + +### Example + +```handlebars + + Currently Idle + + + Currently Loading + + + Idle and loading + +``` + +### See + +- [Component Source Code](./index.js) +- [Template Source Code](./index.hbs) + +--- diff --git a/ui-v2/app/components/state/index.hbs b/ui-v2/app/components/state/index.hbs new file mode 100644 index 000000000..a84864994 --- /dev/null +++ b/ui-v2/app/components/state/index.hbs @@ -0,0 +1,3 @@ +{{#if rendering}} + {{yield}} +{{/if}} \ No newline at end of file diff --git a/ui-v2/app/components/state/index.js b/ui-v2/app/components/state/index.js new file mode 100644 index 000000000..117863d93 --- /dev/null +++ b/ui-v2/app/components/state/index.js @@ -0,0 +1,18 @@ +import Component from '@ember/component'; +import { set } from '@ember/object'; +import { inject as service } from '@ember/service'; + +export default Component.extend({ + service: service('state'), + tagName: '', + didReceiveAttrs: function() { + if (typeof this.state === 'undefined') { + return; + } + let match = true; + if (typeof this.matches !== 'undefined') { + match = this.service.matches(this.state, this.matches); + } + set(this, 'rendering', match); + }, +}); diff --git a/ui-v2/app/helpers/state-matches.js b/ui-v2/app/helpers/state-matches.js new file mode 100644 index 000000000..354898fcc --- /dev/null +++ b/ui-v2/app/helpers/state-matches.js @@ -0,0 +1,9 @@ +import Helper from '@ember/component/helper'; +import { inject as service } from '@ember/service'; + +export default Helper.extend({ + state: service('state'), + compute([state, values], hash) { + return this.state.matches(state, values); + }, +}); diff --git a/ui-v2/app/services/state.js b/ui-v2/app/services/state.js new file mode 100644 index 000000000..deeb3dacb --- /dev/null +++ b/ui-v2/app/services/state.js @@ -0,0 +1,14 @@ +import Service from '@ember/service'; +export default Service.extend({ + matches: function(state, matches) { + const values = Array.isArray(matches) ? matches : [matches]; + return values.some(item => { + return state.matches(item); + }); + }, + state: function(cb) { + return { + matches: cb, + }; + }, +}); diff --git a/ui-v2/tests/integration/components/state-test.js b/ui-v2/tests/integration/components/state-test.js new file mode 100644 index 000000000..814b02cbc --- /dev/null +++ b/ui-v2/tests/integration/components/state-test.js @@ -0,0 +1,33 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | state', function(hooks) { + setupRenderingTest(hooks); + + test('it renders', async function(assert) { + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.set('myAction', function(val) { ... }); + + this.set('state', { + matches: function(id) { + return id === 'idle'; + }, + }); + await render(hbs` + + Currently Idle + + `); + + assert.equal(this.element.textContent.trim(), 'Currently Idle'); + await render(hbs` + + Currently Idle + + `); + + assert.equal(this.element.textContent.trim(), ''); + }); +}); diff --git a/ui-v2/tests/integration/helpers/state-matches-test.js b/ui-v2/tests/integration/helpers/state-matches-test.js new file mode 100644 index 000000000..c9bbdd058 --- /dev/null +++ b/ui-v2/tests/integration/helpers/state-matches-test.js @@ -0,0 +1,32 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Helper | state-matches', function(hooks) { + setupRenderingTest(hooks); + + // Replace this with your real tests. + test('it returns true/false when the state or state in an array matches', async function(assert) { + this.set('state', { + matches: function(id) { + return id === 'idle'; + }, + }); + + await render(hbs`{{state-matches state 'idle'}}`); + assert.equal(this.element.textContent.trim(), 'true'); + + await render(hbs`{{state-matches state 'loading'}}`); + assert.equal(this.element.textContent.trim(), 'false'); + + await render(hbs`{{state-matches state (array 'idle' 'loading')}}`); + assert.equal(this.element.textContent.trim(), 'true'); + + await render(hbs`{{state-matches state (array 'loading' 'idle')}}`); + assert.equal(this.element.textContent.trim(), 'true'); + + await render(hbs`{{state-matches state (array 'loading' 'deleting')}}`); + assert.equal(this.element.textContent.trim(), 'false'); + }); +}); diff --git a/ui-v2/tests/unit/services/state-test.js b/ui-v2/tests/unit/services/state-test.js new file mode 100644 index 000000000..70dd5e099 --- /dev/null +++ b/ui-v2/tests/unit/services/state-test.js @@ -0,0 +1,27 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Service | state', function(hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('.state creates a state matchable object', function(assert) { + const service = this.owner.lookup('service:state'); + const actual = service.state(id => id === 'idle'); + assert.equal(typeof actual, 'object'); + assert.equal(typeof actual.matches, 'function'); + }); + test('.matches performs a match correctly', function(assert) { + const service = this.owner.lookup('service:state'); + const state = service.state(id => id === 'idle'); + assert.equal(service.matches(state, 'idle'), true); + assert.equal(service.matches(state, 'loading'), false); + }); + test('.matches performs a match correctly when passed an array', function(assert) { + const service = this.owner.lookup('service:state'); + const state = service.state(id => id === 'idle'); + assert.equal(service.matches(state, ['idle']), true); + assert.equal(service.matches(state, ['loading', 'idle']), true); + assert.equal(service.matches(state, ['loading', 'deleting']), false); + }); +});