Skip to content

Key concepts

When developing for the Lime CRM web client, understanding certain key concepts is pivotal to maximizing the platform's potential. These central principles and libraries provide the foundation for the customization capabilities of the web client.

Web Components and Integration

Custom web components serve as the core building blocks for Lime CRM's web client customizations. These are self-contained, reusable components built using HTML, CSS, and TypeScript. Each component encapsulates its own behavior and rendering, making them highly modular and easy to maintain.

In Lime CRM's web client, these components are dynamically injected into the application based on their configurations set within Lime CRM Admin. This level of configurability allows custom components to be placed in various application locations including the startpage, object card, custom tabs, table rows, and more. Developers can even create new application routes dedicated to their custom components, providing further opportunities for creating a tailored user experience.

Platform and Context

Understanding the concepts of platform and context is critical to effectively interfacing with the Lime CRM web client.

  • The platform property acts as a service container providing a means to access all services implemented in the web client. It represents the developer's gateway to the rich functionality of the Lime CRM platform. This property is automatically injected by the web client when a component is rendered, meaning developers don't have to manually provide it.
  • The context property provides crucial environmental information about where a component is being rendered. This information can be multi-faceted, ranging from being empty (when no specific contextual information is available) to containing the name of the limetype or both the limetype and an ID for specific lime objects. Importantly, contexts can also be nested, meaning they can maintain a reference to the parent context, allowing for complex, multi-level interactions within the application.

These are provided by the web client as properties on the component. They provide the necessary environment for the component to interact correctly with the Lime CRM platform.

It's important to note that while the web client automatically provides these properties to the root level components, any nested components within your customizations will not automatically receive these properties. If a developer creates a customization with nested components, they are responsible for manually passing down these properties to their child components. This ensures that all components, regardless of their depth in the customization hierarchy, have access to the necessary platform and context information.

Loader Component

The loader component serves as an important point of initialization for customizations in Lime CRM's web client. It is automatically created when the application starts and acts as the go-to location for developers to include any initialization code that their customizations require.

Platform Compatibility

Loaders are created only if the project specifies compatibility with the current platform. This compatibility is indicated by adding the name of the platform to a list in the package.json file. The available platform values include:

  • LimeCRMWebClient
  • LimeCRMDesktopClient
  • LimeCRMWebAdminClient

Example

package.json
{
    "lime": {
        "platforms": ["LimeCRMWebClient", "LimeCRMWebAdminClient"]
    }
}

If a project is compatible with multiple platforms, the project's loader component should check the platform.type property. It's essential to recognize that the initialization process may vary depending on the platform.

In practical terms, this means that developers can tailor their initialization code within the loader component to accommodate the specific requirements of each supported platform. By doing so, customizations can seamlessly adapt to different Lime CRM environments, enhancing flexibility and usability.

Initialization Tasks

In the loader component, developers typically perform various initialization tasks, such as:

  • Registering Commands:
    Commands represent a user's intent and are triggered based on user interactions. Developers can define and register commands and their corresponding handlers within the loader component. For example:

    const myHandler = new MyHandler();
    commandbus.register(MyCommand, myHandler);
    
  • Registering Event Listeners:
    To respond to specific events happening within the application, developers can register event listeners within the loader component. For instance:

    eventDispatcher.addListener('navigate', (event) => {
        console.log(`Navigating to ${event.detail}`);
    });
    
  • Registering New Routes:
    If developers wish to create new routes in the application to display their custom components, these routes are registered within the loader component. An example would be:

    routeRegistry.registerRoute('/my-component/:param1', 'my-component');
    

By housing initialization logic within the loader component, developers can keep their customizations organized and ensure their setup code is executed at the appropriate time in the application's lifecycle. This approach leads to more predictable and maintainable customizations.

Actions and Commands

The use of commands and actions is a powerful approach within Lime CRM's web client to effectively encapsulate user intent. It enables the representation of all necessary information to perform an action within an object. This object (the command) can then be executed at a later point without needing to know the specifics of the requested action or its receiver.

This is facilitated through the commandbus service provided by the platform. Implementing the command design pattern in combination with a service layer, the commandbus serves as a conduit between user intent (the command) and the action's implementation (the handler).

The commandbus will match each command object to its appropriate handler, effectively allowing you to neatly structure your code with clear separations of concerns. This is particularly useful in situations where a component, such as a menu, needs to respond to user interactions without explicitly knowing the underlying actions.

A menu component, for instance, only takes command objects as input and doesn't need to know what actions to perform when a user selects an item. It simply sends the commands to the commandbus for execution when an item is selected. This approach provides a clean and modular way to structure your customizations.

Commands are often associated with actions, which are essentially a UI representation of these commands. Actions can be configured with an icon, color, title, etc., allowing the command to be presented in a user-friendly manner within the application. An action gets registered to the platform, which then takes care of showing it in the appropriate places based on its configuration.

Controlling Visibility of Components

There might be scenarios where the visibility of a web component in a slot is conditional, that is, it depends on specific conditions. For instance, different sets of components might need to be displayed to different teams within an organization, such as the marketing team and the support team. To manage such visibility of components efficiently and avoid gaps, the application of specific CSS rules becomes crucial.

The visibility of a component can be controlled by setting the CSS style display: none on the component's :host element. This technique effectively hides the component while maintaining the structure of the slot, thereby avoiding any undesired gaps.

The following code snippets illustrate how to apply this technique:

First, define the CSS rule in your component's styles:

:host(.hidden) {
    display: none;
}

Then, in your component's TypeScript file, dynamically assign the hidden class to the :host element based on your condition:

import { Component, h, Host } from '@stencil/core';

export class MyComponent {
    public render() {
        if (this.isHidden()) {
            return (
                <Host class={{ hidden: true }}>
                    // No need to render component since it's hidden
                </Host>
            );
        }

        return (
            // Render component as normal
        )
    }

    private isHidden(): boolean {
        // return true or false based on your specific condition
    }
}

In the above TypeScript code, this.isHidden() should be a method that returns true or false based on your specific condition. If true, the hidden class is applied to the host element and the component is hidden. If false, the hidden class is not applied and the component remains visible.

By leveraging this approach, you can dynamically control the visibility of your web components based on any condition, providing a flexible and effective way to cater to varying display requirements.

Web Client State and Data Access

The Lime CRM web client maintains a global state that holds all its data, such as limetypes, limeobjects, configs, etc. Accessing this data is a critical aspect of developing customizations, and the platform provides a simple and efficient mechanism for doing so.

To access data from the global state, components use decorators to decorate the properties inside their components. These properties are bound in a one-way manner: data flows from the state to the component. When data changes in the application's state, these changes are automatically reflected in the decorated properties and trigger the component to re-render.

Here's an example of how a component can access a specific limeobject:

class MyComponent {
    @State()
    @SelectCurrentLimeObject()
    private myLimeObject: LimeObject;
}

In this scenario, the myLimeObject property will be automatically updated whenever the corresponding limeobject in the application's state changes.

It's important to note that this is a one-way binding: changes made to the properties within the component will not reflect back to the state. To update data in the state, the component must emit specific events or interact with the platform's services. These actions are taken care of by the platform, which synchronizes the changes back to the global state.

Additionally, Lime CRM has an automatic syncing mechanism for limeobjects. A listener in the Unit of Work in the backend tracks changes to all objects and sends the updated data back to the client automatically. This means that if a developer updates an object through code in a custom endpoint part of their customization, they do not need to manually sync this data with the web client. The system takes care of this automatically, ensuring that the data presented in the web client is always up to date with the backend data.

Understanding this one-way data binding mechanism is vital for building efficient and predictable customizations in Lime CRM's web client. It promotes data consistency across the application while providing an intuitive interface for customizations to interact with the application's data.

Fine-tuning Data Access with Arguments and Mapping Functions

Using Arguments for Filtered Results

In many cases, we might need to specify certain conditions or filters to narrow down the data retrieved. This can be accomplished by sending arguments into the decorator. For example, if we want to load all currently loaded limeobjects of a specific limetype, we can specify that limetype when decorating the property.

export class MyComponent implements LimeWebComponent {
    @State()
    @SelectLimeObjects({
        limetype: "person",
    })
    private persons = [];
}

In the example above, the decorator will fetch all currently loaded objects of the limetype "person".

Leveraging Mapping Functions

A mapping function can further enhance the utility of these decorators by allowing more complex and specific transformations of the returned data. Assume we have a web component that is displayed on the company card, and we want a list of all persons connected to that company. We can define a mapping function that filters out our required persons:

function currentPersons(this: LimeWebComponent, persons = []) {
    return persons.filter((person) => person.company === this.context.id);
}

This function filters out the list of persons related to the company in the current context. We can apply this mapping function by specifying it when decorating our property.

export class MyComponent implements LimeWebComponent {
    @State()
    @SelectLimeObjects({
        limetype: "person",
        map: [currentPersons],
    })
    private persons = [];
}

The mapping function modifies the original result (a list of all persons) to a new result (a filtered list of persons) that is stored in the connected property.

Code Quality and Separation of Concerns

One key principle to keep in mind when developing for Lime CRM's web client is the separation of concerns. While it may be tempting to house all logic within a single web component, this can lead to bloated, hard-to-maintain components over time.

Instead, developers should aim to keep their web components as simple as possible, focusing on the presentation logic within the components themselves and offloading business logic to separate files or services. This results in cleaner, more manageable code and allows for better reusability across the platform.

Lime Libraries

Lime CRM provides a suite of libraries to aid in the development of custom web components:

  • Lime Elements: This is a comprehensive design system containing a plethora of reusable UI components such as buttons, lists, tables, dialogs, and more. These components, while designed with Lime CRM in mind, are agnostic to the platform and could easily be repurposed for other products, making Lime Elements a versatile tool in a developer's toolbox.

  • Lime Web Components: This library offers types and interfaces for all services on the platform, providing a clear contract for the services that can be expected on the platform. This also includes a variety of helper functions that simplify many common tasks in developing web components.

  • Lime CRM Building Blocks: This is a collection of reusable components crafted specifically for Lime CRM. It merges the foundational aspects of Lime Elements with the contextual relevance of Lime Web Components, providing developers with a powerful tool for creating Lime CRM-specific solutions.

  • limeclient.js: The implementation of all service interfaces defined in Lime Web Components falls under this package. While it typically operates behind the scenes, limeclient.js plays a more active role in the process of writing unit tests.

These libraries together form the fundamental toolkit for any developer working with Lime CRM's web client, providing a powerful, flexible, and streamlined development experience.

By fully grasping these key concepts, developers will be well-equipped to exploit the full potential of the Lime CRM web client and deliver high-quality custom solutions.