Services¶
The Lime CRM Web Client platform provides a variety of services that you can leverage in your customizations. These services are like tools in a toolbox, each designed to carry out specific tasks within the application. This chapter is designed to give you an introduction to some of the most commonly used services in Lime CRM and demonstrate how they can be used when developing customizations.
Understanding the services and how to utilize them in your customization can greatly enhance the efficiency of your code and the functionality of your customization. Each service is designed with specific tasks in mind and is often designed to integrate seamlessly with other services.
The services are essentially interfaces that expose specific methods for interacting with various aspects of the Lime CRM Web Client platform. The services are provided by the platform, and can be accessed within your web components through the platform
property.
In the following sections, we will delve into some of these services, their roles, how they interact with other services, and how to use them effectively in your customization code. We'll provide code examples to illustrate typical usage of these services. Note, however, that these are just a starting point. As you gain a deeper understanding of each service, you'll likely find more advanced and innovative ways to use them in your customizations.
Commandbus¶
The commandbus service is a pivotal part of the Lime CRM platform, enabling the encapsulation of user intent into executable actions. The command design pattern combines with a service layer to create a clean, understandable structure that enables the separation of the command (user intent) from the handler (the execution of that intent).
This separation allows user interactions to be decoupled from their implementation. You create commands that encapsulate all the information needed to perform an action, and corresponding handlers that will carry out that action when required.
Let's break down the steps to create and use commands with the commandbus service.
-
Creating a Command
Commands can be scaffolded using Lime Project. When you generate a new command, it will create a new TypeScript file with a class that is decorated with the
Command
decorator.For instance, suppose you want to create a command called
SayHelloCommand
. You can create it as follows:import { Command } from '@limetech/lime-web-components'; @Command({ id: 'my-plugin.hello' }) export class HelloCommand { constructor(public name: string) {} }
Here, the
id
is a unique string identifier for the command and thename
is the data required to execute the command. -
Creating a Command Handler
The handler of a command is responsible for executing the command. It's a separate class that implements the
CommandHandler
interface.import { CommandHandler, Notifications } from '@limetech/lime-web-components'; export class HelloHandler implements CommandHandler { constructor(private notifications: Notifications) {} public handle(command: HelloCommand): void { this.notifications.alert('Hello', `Hello, ${command.name}`); } }
In this example, the
HelloHandler
handles theHelloCommand
. When invoked, it opens an alert saying hello to the user. -
Registering the command on the commandbus
Commands and their handlers need to be registered on the commandbus to be invoked. This registration process is typically done in the loader component.
import { Component } from '@stencil/core'; import { LimePluginLoader, PlatformServiceName } from '@limetech/lime-web-components'; import { HelloCommand } from '../commands/hello.command'; import { HelloHandler } from '../commands/hello.handler'; @Component({ tag: 'my-plugin-loader', shadow: true, }) export class Loader implements LimePluginLoader { public componentWillLoad() { const helloHandler = new HelloHandler(this.notifications); this.commandbus.register(HelloCommand, helloHandler) } private get commandbus() { return this.platform.get(PlatformServiceName.CommandBus); } private get notifications() { return this.platform.get(PlatformServiceName.Notifications); } }
The
register
method takes two arguments: the command class and an instance of the command handler.Once the command is registered, you can use the
handle
method of the commandbus to invoke it anywhere in the application:
This structure of commands and their handlers allows for the creation of clear, well-structured code where user intent is separated from the execution of those intents. It offers a clean, simple way of creating user interactions that is easy to maintain and extend over time.
EventDispatcher¶
The event dispatcher service is a vital part of the Lime CRM platform, allowing the customization to listen for various application events. It uses an event-driven architecture pattern, which provides a highly decoupled, highly scalable system design.
Using the event dispatcher, customization developers can subscribe to specific events happening within the system and react accordingly. This can be used, for instance, to trigger custom actions, or even to prevent certain events from being handled, allowing developers to override system defaults with their own commands or handlers.
Here's a basic overview of how to use the event dispatcher:
-
Subscribing to an Event
To start listening to a particular event, you can subscribe to it using the
addListener
method of theEventDispatcher
. TheaddListener
method requires two parameters: the name of the event to listen to, and a callback function to execute when the event occurs.For instance, if you want to listen to the event that occurs when a command is being handled, you can do this as follows:
import { PlatformServiceName, CommandEventName, CommandEvent } from '@limetech/lime-web-components'; export class MyCustomComponent { public connectedCallback() { this.eventDispathcer.addListener(CommandEventName.Received, this.handleCommandReceived); } private handleCommandReceived = (event: CommandEvent): void => { console.log('Command received:', event.detail.command); }; private get eventDispatcher() { this.platform.get(PlatformServiceName.EventDispatcher); } }
In this case, the
handleCommandReceived
method will be invoked each time a command is reveived on the command bus. -
Unsubscribing from an Event
When your component is destroyed, you should also stop listening to the events to avoid memory leaks. This can be done by calling the
removeListener
method. Listeners that are added in the loader component does not typically have to be removed since the loader will live for the entire lifespan of the application. -
Preventing Events
In some scenarios, you might want to prevent an event from being handled by the default system handlers. This can be done by calling the
preventDefault
method of the event object inside your event handler:export class MyCustomComponent { private handleCommandReceived = (event: CommandEvent): void => { if (event.detail.command instanceof HelloCommand) { event.preventDefault(); this.commandbus.handle(new GoodbyeCommand()); } }; }
In this example, if a
HelloCommand
has been received, it will be stopped from being handled and instead aGoodbyeCommand
will be handled instead.
The event dispatcher service is a powerful tool for customization developers, allowing for fine-grained control over system events and the ability to trigger custom behavior based on those events.
Dialogs¶
The dialog renderer in Lime CRM provides an easy way to render interactive modal dialogs from your customizations. This can be especially useful when a command is triggered that requires user interaction or input before proceeding. The dialog service simplifies this by providing a built-in way to create and manage these dialogs.
First, let's suppose we have a command handler for the command OpenDialogCommand
. The command handler uses the dialog service to open a dialog:
import { OpenDialogCommand } from '../commands/open-dialog.command';
import { CommandHandler, PlatformServiceName, DialogRenderer } from '@limetech/lime-web-components';
export class OpenDialogCommandHandler implements CommandHandler {
constructor(private dialogRenderer: DialogRenderer) {}
public handle(command: OpenDialogCommand) {
this.dialogService.create(
'my-dialog-component',
{
data: command.data,
},
{
submit: this.handleSubmit,
cancel: this.handleCancel,
}
);
}
handleSubmit = (event: CustomEvent) => {
console.log('Submit event received with data:', event.detail);
// Handle submit event
}
handleCancel = (event: CustomEvent) => {
console.log('Cancel event received with data:', event.detail);
// Handle cancel event
}
}
Here, the OpenDialogCommandHandler
opens a dialog that renders the component my-dialog-component
. Two event listeners, handleSubmit
and handleCancel
, are also passed to the component. These functions will handle the submit
and cancel
events that the dialog component may emit.
Next, the implementation of the my-dialog-component
can be something like this:
import { Component, h, Prop, Event, EventEmitter } from '@stencil/core';
@Component({
tag: 'my-dialog-component',
shadow: true,
})
export class DialogComponent {
@Prop()
public data: any;
@Event()
public submit: EventEmitter<any>;
@Event()
public cancel: EventEmitter<void>;
@State()
private newData: any;
public render() {
return (
<limel-dialog open={true} heading="My Dialog">
{this.renderBody()}
{this.renderFooter()}
</limel-dialog>
);
}
public renderBody() {
// Render dialog body based on this.data
}
public renderFooter() {
return (
<div slot="button">
<limel-button label="Submit" onClick={this.handleSubmit} />
<limel-button label="Cancel" onClick={this.handleCancel} />
</div>
);
}
public handleSubmit = () => {
this.submit.emit(this.newData);
}
public handleCancel = () => {
this.cancel.emit();
}
}
Here, the my-dialog-component
web component is essentially a wrapper around the limel-dialog
component. It uses properties passed to it to render its content, and it emits events based on user interactions.
This is a common way of using the dialog service in conjunction with commands and handlers.
Forms¶
A common scenario while creating customizations involves rendering forms inside dialogs. These forms often require schemas that are created in the backend. Thus, developers usually use the HttpClient service to fetch the schema, and then provide it to the form component inside the dialog.
Moreover, after the form is submitted, data can be sent to a custom endpoint using the HttpClient.
Let's illustrate this with a simple example:
export class OpenFormDialogCommandHandler implements CommandHandler {
constructor(
private dialogRenderer: DialogRenderer,
private httpClient: HttpClient
) {}
async handle(command: OpenFormDialogCommand) {
const formSchema = await this.fetchFormSchema();
this.dialogRenderer.create(
'my-form-dialog-component',
{
schema: formSchema
},
{
submit: this.handleSubmit
}
);
}
private fetchFormSchema = () => {
return this.httpClient.get('my-api/form-schema');
};
private async handleSubmit(event: CustomEvent) {
const response = await this.httpClient.post('my-api/submit-form', event.detail);
console.log('Form submitted successfully with response:', response);
}
}
In the command handler above, when handling the OpenFormDialogCommand
, we first fetch the form schema. We then open the dialog and provide the form schema as a prop to the dialog. The handler listens for the submit
event from the dialog, and the form data is submitted using the httpClient
.
import { Component, h, Prop, State, Event, EventEmitter } from '@stencil/core';
import { PlatformServiceName } from '@limetech/lime-web-components';
@Component({
tag: 'my-form-dialog-component',
shadow: true,
})
export class FormDialogComponent {
@Prop()
public schema: any;
@Event()
public submit: EventEmitter;
@Event()
public cancel: EventEmitter;
@State()
private value: any;
public render() {
return (
<limel-dialog
header="My Form Dialog"
body={this.renderBody()}
footer={this.renderFooter()}
/>
);
}
public renderBody() {
if (!this.schema) {
return <limel-spinner />;
}
return (
<limel-form
value={this.value}
schema={this.schema}
onChange={this.handleChange}
/>
);
}
public renderFooter() {
return (
<div slot="button">
<limel-button label="Submit" onClick={this.handleSubmit} />
<limel-button label="Cancel" onClick={this.handleCancel} />
</div>
);
}
private handleChange = (event: CustomEvent) => {
this.value = event.detail;
}
private handleSubmit = () => {
this.submit.emit(this.value);
}
private handleCancel = () => {
this.cancel.emit();
}
}
The my-form-dialog-component
receives the form schema as a prop and renders the form based on the schema. When the form is submitted, the component emits a submit
event.
This way, the command handler can handle the logic of fetching the schema and submitting the form data, while the component handles the presentation. This is a common example of separating the concerns of data fetching and UI rendering.
Configuration¶
The ConfigsRepository
service is a key part of Lime CRM customizations, providing a method to create and access configurations, which are often set up using schemas in Python and scaffolded using lime-project
.
Setting Up Configurations in Python¶
To get started with a new configuration, you can use lime-project
to scaffold it. Run the following command:
This command will generate a configuration schema in Python. The generated Python file will look something like this:
from marshmallow import Schema, fields
import lime_admin.plugins
class MyConfig(lime_admin.plugins.AdminPlugin):
@property
def name(self):
return "my-config"
@property
def title(self):
return "My Config"
@property
def version(self):
return None
def get_schema(self):
class MyConfigSchema(Schema):
title = fields.String(title="Name", required=True)
return MyConfigSchema()
In the above code, we define a configuration schema named MyConfig
that includes one string field (title
)
Accessing Configurations in Your Web Components¶
Once the configuration schema is set up in Python, you can fetch the configuration in your web components using the @SelectConfig
decorator. This decorator binds your configuration to a property in your component. Whenever the configuration changes, the property gets updated automatically.
Here's how to use the @SelectConfig
decorator:
import { Component, h, State } from '@stencil/core';
import { SelectConfig } from '@limetech/lime-web-components';
@Component({
tag: 'my-customization-component',
shadow: true,
})
export class MyCustomizationComponent {
@State()
@SelectConfig({
name: 'my-config'
})
private config: any;
render() {
return (
<div>
<h1>{this.config.title}</h1>
</div>
);
}
}
In the example above, my-config
is the name of the configuration created in Python. We use this name with the @SelectConfig
decorator to bind the configuration to the config
property in the component. This config
property will contain the data of the configuration, which we can use inside our render()
function to display the title from the configuration.
Remember that you can only select configurations that are defined in the Python backend, and you must ensure the name of the configuration in your web component matches the name defined in Python.
Registering Routes with RouteRegistry
and Navigator
Services¶
Beta
This section outlines the process of registering routes within the web client using the RouteRegistry
service. These registered routes can then be navigated using the Navigator
service. These services provide a straightforward mechanism to handle route registration within the application, and facilitate navigation between various locations. Each registered web-component will be rendered in full-screen, with only the navigation dock visible alongside the component itself.
Route Registration¶
To register a new route, utilize the registerRoute
method of the RouteRegistry
service. This should be done when the application starts up, mening it should be done in the loader component for the package. Here is the corresponding syntax:
const routeRegistry = platform.get(PlatformServiceName.RouteRegistry);
routeRegistry.registerRoute('/zoo/:param1/:param2', 'zoo-component');
In this example, the route /zoo/:param1/:param2
is associated with the zoo-component
component. Colons denote route parameters, which become properties on the routed web-component when that route is navigated to. These properties align with the route parameter names. The RouteRegistry
will automatically match the route parameters to the appropriate web-component.
If the route includes query parameters, they will not impact the component matching process. Instead, these will be set as a query
property on the routed web-component. The web-component should implement the RouteComponent
interface to handle type-checking for these properties correctly.
Special Route Parameters
The RouteRegistry
service has a unique handling mechanism for route parameters named limetype
and id
. If both, or just limetype
, are present, a context
property is created on the routed web-component.
Navigation¶
Once a route is registered, it can be navigated to using the Navigator
service. This service manages transitions between different locations within the web client. Another good use case is to create a shortcut and add it to the start page. The following config will create a shortcut and navigate to the component once clicked.
{
"name": "limel-shortcut",
"size": "square-small",
"props": {
"icon": "dog",
"label": "Zoo",
"style": {
"--shortcut-icon-color": "rgb(var(--color-white))",
"--shortcut-background-color": "rgb(var(--color-yellow-dark))"
},
"link": {
"href": "zoo/dog/1001"
}
}
}
For further technical details on these services, please refer to the API documentation.