Skip to content

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.

  1. 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:

    lime-project generate command hello
    
    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 the name is the data required to execute the command.

  2. 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 the HelloCommand. When invoked, it opens an alert saying hello to the user.

  3. 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:

    const command = new HelloCommand('Indiana')
    this.commandbus.handle(command);
    

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:

  1. Subscribing to an Event

    To start listening to a particular event, you can subscribe to it using the addListener method of the EventDispatcher. The addListener 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.

  2. 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.

    export class MyCustomComponent {
        public disconnectedCallback() {
            this.eventDispatcher.removeListener(CommandEventName.Received, this.handleCommandReceived);
        }
    }
    
  3. 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 a GoodbyeCommand 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:

lime-project generate config-module

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.

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.