Research
Security News
Quasar RAT Disguised as an npm Package for Detecting Vulnerabilities in Ethereum Smart Contracts
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
ember-command
Advanced tools
Implementation of the Command design pattern from C-Q-S for ember.
(fn)
and enjoy partial applications (because it stayed a
function)What you'll get:
Command
class to extend from for your implementationLinkCommand
as syntactic sugar for creating a link (through ember-link
)@command
decorator to connect your component with your command<CommandElement>
component as your building block to attach your command to the UI<CommandElement>
will accept a Command
, an @action
or a (link)
<CommandElement>
will render the correct HTML element to take care of
accessibilityember install ember-command
This section will teach on preparing your UI components, writing commands, attaching them your UI and testing them.
This documentation is guided by product development and engineering for Super Rentals Inc with CPO Tomster and Frontend Engineer Zoey.
These passages are optional, you are free to skip - but may feel very much related to your daily work.
Tomster realized a cohort of customers that are interested in seeing personalized recommendations for rentals and put together a specification for the feature. Super Rentals shall be extended with a recommendation section offering personalized exposé to customers. Customers can request offers and learn more about the object.
Tomster and Zoey underlined the relevant nouns and verbs in the feature specification to draw the domain terminology from it. The recommendation is the new aggregate and request offer and learn more are the actions upon that.
Meanwhile the backend developers were busy delievering an endpoint that implements the business logic for these actions. Now Zoey's job is to connect the UI to these endpoints. To dispatch the request, the
data
service is used.
All elements/components that you want to invoke commands must be prepared.
Gladly you don't have to deal with the implementation details, this is what the
<CommandElement>
is for. Your job is to integrate this component into your
existing set of components. WAI-ARIA 1.1 for Accessible Rich Internet
Applications explicitely mentions button
,
menuitem
and link
as the implementationable roles for the abstract super role
command
(but there also may be
more UI elements, that are receivers of commands).
Let's make an example button component with the help of <CommandElement>
:
{{! components/button/index.hbs }}
<CommandElement @command={{@push}} ..attributes>
{{yield}}
</CommandElement>
and give it an interface to describe the arguments:
// components/button/index.d.ts
import Component from '@glimmer/component';
import { Command } from 'ember-command/components/command-element';
export interface ButtonArgs {
/** A command which will be invoked when the button is pushed */
push: Command;
}
which we can use as:
<Button @push={{this.ourCommand}}>Request an Offer</Button>
Yes a button is pushed not onClicked (Think about it: Do you push or click a button/switch to turn on the lights in your room?).
As Zoey is caring about accessibility, she wants commands to be represented as its appropriate element.
Thanks to the <CommandElement>
the rendered element will adjust to either
<a>
or <button>
and will cover all accessbility needs out of the box for you.
By handing off that logical part to <CommandElement>
you can focus on giving
your button component the best styling it deserves ;)
This section focusses on writing commands in various formats.
Ember recommends to use @action
decorator for writing functions that can be
invoked from UI elements. ember-command
is built to work with these existing
mechanismns. Any regular @action
function/method also qualifies
as command and your existing code continues to work as is:
import { inject as service } from '@ember/service';
import Component from '@glimmer/component';
import DataService from 'super-rentals/services/data';
class RecommendationComponent extends Component {
@service declare data: DataService;
@action
requestOffer() {
// very whimsical things here
}
}
Yet, in this case business logic is coupled to the component. To write clean code, we want to have this separated and let the component be the glue part connecting business logic with UI.
As of that the simplest example is to extract your business logic into a function:
import { inject as service } from '@ember/service';
import Component from '@glimmer/component';
import DataService from 'super-rentals/services/data';
import { requestOffer } from 'super-rentals/recommendations';
class RecommendationComponent extends Component {
@service declare data: DataService;
@action
requestOffer() {
requestOffer(this.data);
}
}
Extracting into functions is a good step to write maintainable code by
applying separation of concerns. Functions are nice in a way they are isolated and
work only with the parameters passed into them (unless the outer scope is
accessed or a function is run within a specific object - yes EmberRouter
I
mean your parameter to
map()
).
As the purpose of a command is to mutate the system, passing in all dependencies
can be quite cumbersome.
As a matter of that functions are great to query the system and request a particular state about something. Commands are there to cause side-effects to the system. Carefully using either one of them leads to proper command and query separation.
Commands interact with the system, they are better contained in classes and
their dependencies can be fulfilled through dependency injection. That's
what the Command
base class is for. Here is how we write our command from
above with access to the data layer (an ember service) to fire off a command
to the backend:
import { inject as service } from '@ember/service';
import { Command } from 'ember-command';
import DataService from 'super-rentals/services/data';
export default class RequestOfferCommand extends Command {
@service declare data: DataService;
execute(): void {
this.data.sendCommand('super-rentals.recommendations.request-offer', {...});
}
}
and we use the component to connect our command with the UI:
// components/recommendation
import Component from '@glimmer/component';
import { command } from 'ember-command';
import RequestOfferCommand from 'our-module-above';
class RecommendationComponent extends Component {
@command requestOffer = new RequestOfferCommand();
}
We connect the command to our component by using the @command
decorator, which
attaches the owner to the command and enables dependency injection onto it and
wraps the command in a function that, when invoked, will call the execute()
method of the command.
You may realize, this is an implementation of the command design pattern.
To seed commands, the constructor can be used. We extend our component with an argument and pass it down to the command:
// components/recommendation
interface RecommendationArgs {
recommendation: Expose;
}
class RecommendationComponent extends Component<RecommendationArgs> {
@command requestOffer = new RequestOfferCommand(this.args.recommendation);
}
and expect it from our command:
import { inject as service } from '@ember/service';
import { Command } from 'ember-command';
import DataService from 'super-rentals/services/data';
export default class RequestOfferCommand extends Command {
@service declare data: DataService;
#recommendation: Expose;
constructor(recommendation: Expose) {
this.#recommendation = recommendation;
}
execute(): void {
this.data.sendCommand('super-rentals.recommendations.request-offer', {
recommendation: this.#recommendation,
});
}
}
Now the command can operate on the recommendation aggregate.
Hello Zoey? It's Tomster, our data and analytics team entered a late change to the original feature, they want to add tracking onto the link to measure the impact of that feature. Can you add tracking, too?
"sure" answers Zoey as she is confident to add this change with surgery precision into the already existing code, keeping the level of achieved separation. For tracking purposes, she knows, there is a
tracking
service to use.
We can have compound commands executed when a UI element is invoked, each in its own class. Let's add the tracking command:
import { inject as service } from '@ember/service';
import { Command } from 'ember-command';
import TrackingService from 'super-rentals/services/tracking';
export default class TrackRequestOfferCommand extends Command {
@service declare tracking: TrackingService;
#recommendation: Expose;
constructor(recommendation: Expose) {
this.#recommendation = recommendation;
}
execute(): void {
this.tracking.track('recommendations.request-offer', {
recommendation: this.#recommendation,
});
}
}
And the only change need to make to our existing code to integrate the tracking command:
// components/recommendation
import Component from '@glimmer/component';
import { command } from 'ember-command';
import RequestOfferCommand from 'our-module-above';
+ import TrackingRequestOfferCommand from 'our-other-module';
interface RecommendationArgs {
recommendation: Expose;
}
class RecommendationComponent extends Component<RecommendationArgs> {
- @command requestOffer = new RequestOfferCommand(this.args.recommendation);
+ @command requestOffer = [
+ new RequestOfferCommand(this.args.recommendation),
+ new TrackingRequestOfferCommand(this.args.recommendation)
+ ];
}
Zoey got notice from her coworker, who implemented a details route to which the learn more action should link to.
Commands can also be links, which the <CommandElement>
will render as <a>
element. The best solution for creating links is the
ember-link
addon.
Programmatically creating links with ember-link is a bit of a
mouthful, like so:
class RecommendationComponent extends Component {
@service declare linkManager: LinkManagerService;
get learnMoreLink() {
return this.linkManager.createUILink({ route: 'recommendation.details' });
}
}
Passing learnMoreLink
to @push
at our button would work straight ahead.
ember-command
comes with a more friendly syntax to create links
programmatically for commands, which is the LinkCommand
and be used as:
import { command, LinkCommand } from 'ember-command';
class RecommendationComponent extends Component {
@command leanMoreLink = new LinkCommand({ route: 'recommendation.details' });
}
so much more lean :)
Hey Zoey, it's Tomster again - can you also add tracking to the learn more link?
Compound commands work with links, too. Constructed as an array, as already used above with multiple commands:
class RecommendationComponent extends Component {
@command leanMoreLink = [
new LinkCommand({ route: 'recommendation.details' }),
new TrackLearnMoreCommand(this.args.recommendation),
];
}
Whenever there is a link command present, the <CommandElement>
will render as
<a>
. When there are multiple links present, the first one will be rendered,
all successive ones will be dropped.
This is straight forward. Let's take our recommendation component, which has a
requestOffer
and a learnMore
action to attach to the UI:
<Button @push={{this.requestOffer}}>Request offer</Button>
.. and somewhere else ..
<Button @push={{this.learnMore}}>Learn more</Button>
Of course, requestOffer
can be any format mentioned under writing
commands section. Also for links, you have a chance to do
this in a template-only style:
<Button @push={{link "recommendation.details"}}>Learn more</Button>
Just use the flavor you like the most.
As commands are isolated and self-containing a business logic, we can write
tests to specifically test for this. Let's test the tracking command using
ember-qunit-sinon
to stub
our service:
import { setupTest } from 'ember-qunit';
import { module, test } from 'qunit';
import { prepareCommand } from 'ember-command/test-support';
import { TestContext } from 'ember-test-helpers';
import sinon from 'sinon';
import TrackingRequestOfferCommand from 'our-module';
module('Integration | Command | TrackingRequestOfferCommand', function (hooks) {
setupTest(hooks);
test('it tracks', async function (this: TestContext, assert) {
this.owner.register('service:tracking', TrackingService);
const trackingService = this.owner.lookup('service:tracking');
const stub = sinon.stub(trackingService, 'track');
const cmd = prepareCommand(this, new TrackingRequestOfferCommand());
cmd.execute();
assert.ok(stub.calledOnce);
});
});
The prepareCommand
is the testing equivalent to the @command
decorator to
attach the owner and wires up dependency injection.
See the Contributing guide for details.
This project is licensed under the MIT License.
FAQs
Commands for Ember
The npm package ember-command receives a total of 237 weekly downloads. As such, ember-command popularity was classified as not popular.
We found that ember-command demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
Security News
Research
A supply chain attack on Rspack's npm packages injected cryptomining malware, potentially impacting thousands of developers.
Research
Security News
Socket researchers discovered a malware campaign on npm delivering the Skuld infostealer via typosquatted packages, exposing sensitive data.