Lime Web Components¶
Lime Web Components are UI components you can embed in the Web Client. Components are built using StencilJS, which uses TypeScript, JSX and reactive data-binding. A basic understanding of these concepts is required in order to follow along this documentation.
Creating a new web component¶
To create a new web component you need a package created with lime-project
. Once a package has been created, a new web component can be created by executing the following from the package root directory:
lime-project generate web-component <my-component>
where <my-component>
is the name of the component.
Starting the dev environment¶
Once a web component has been created, go to the <package>/frontend
directory inside the package and run:
npm install
Then restart the Lime CRM Webserver and run:
npm start
This command will create a development build of your web-component and watch for any changes in the frontend
folder.
Commands¶
Commands are a great way to help capture user intent. They let us encapsulate all information needed to perform an action into an object, and later execute that action without knowing anything about the action beeing requested or the receiver of the request.
To help with this, the platform exposes a service called commandbus
.
Command bus¶
The command bus uses the command design pattern and combines it with a service layer. Its job is to take a command object (which describes what the user wants to do) and match it to a handler (which executes it). This can help you structure your code in a neat way.
We can for instance use it in a menu. The menu knows nothing about what actions to perform when the user selects an item in the menu. It only takes command objects as input, and sends the commands to the command bus for execution when an item is selected.
Example¶
First we declare and register the command and its handler on the command bus
@Command({ id: "my-special-command" })
class MySpecialCommand {
public id: number;
public limetype: Limetype;
}
class MySpecialHandler implements CommandHandler {
public handle(command: MySpecialCommand) {
// do your magic stuff here
}
}
const handler = new MySpecialHandler();
const commandBus = platform.get(PlatformServiceName.CommandBus);
commandbus.register(MySpecialCommand, handler);
Then, if we want to invoke the command, we simply create an instance of it and send it to the commandbus
const command = new MySpecialCommand();
command.id = 1001;
command.limetype = { ... };
commandbus.handle(command);
Context¶
The context property on LimeWebComponent
contains the context the component belongs to. This context depends on where the component lives. For most cases, it will just contain a limetype
and an id
, but in some cases there might not be any context information available. It's the responsibility of the webclient to pass a valid context to the web component.
Lime Elements¶
Lime Elements are available for Lime Web Components and should be used to get the same look and feel as the rest of the webclient.
For more information regarding Lime Elements, please refer to the Lime Elements documentation.
Lime Web Component¶
A root component is a component that lives directly within the webclient and is not nested within any other component. All root components have to implement the LimeWebComponent
interface in order to work properly and the webclient will pass down the required properties when the web component is created.
Components that are not root components are not required to implement the LimeWebComponent
interface. However, if they make use of the context or any of the platform services, these have to be passed down explicity by the consumer. Components that make use of the state decorators are required to fully implement LimeWebComponent
.
Platform services¶
The platform property on LimeWebComponent
exposes all services that the Lime CRM webclient provides for plugins to make use of.
List of services¶
Name | Description |
---|---|
state | Access state information from the webclient |
translate | Translate strings to other languages |
http | Make requests to custom endpoints |
route | Redirect to other parts of the webclient |
notifications | Show simple dialogs and notification messages |
query | Execute queries using the query objects API |
commandbus | Register and execute commands |
dialog | Open custom modal dialogs |
eventDispatcher | Handle application level events |
Slots¶
There are a number of slots in the Lime CRM webclient where web components can be added. All of them are configured from the administrator page inside the webclient. Slots can either be slots where components are rendered, or inside menus where actions can be added.
Hiding components¶
Sometimes you want to show or hide a web component in a slot depending on a specific condition. Maybe the marketing team should see one set of components and the support team should see another set of components. In order to avoid gaps in our slots, you must ensure that the correct css rules are set when a component should be hidden. You do that by setting the css style display:none
on the component's :host
element. See example code below:
:host.hidden {
display: none;
}
import { Component, h, Host, … } from '@stencil/core';
…
public render() {
return (
<Host class={{ hidden: this.hidden }}>
…
</Host>
);
};
…
Deprecated slots¶
In previous versions of Lime CRM webclient, web components could be directly injected into the client by an addon. This way of configuring web components is now deprecated and will be removed in the future. To specify that a web component should appear in one of these deprecated slots, edit the file lwc.config.json
in the web component's package.
The format should be:
[
{
"name": "<component-name>",
"slot": "<slot-name>"
}
]
where <component-name>
is the tag
name of the web component specified in the @Component
decorator, and <slot-name>
is the name of the slot.
Only root components should be listed in lwc.config.json
, i.e. components that are created and injected by the webclient. Other components, e.g. components that are nested or reused between many components, do not belong in any slot and should not be listed.
List of slots¶
The following slots were previously defined for Lime CRM webclient. They should no longer be used.
Slot | Location |
---|---|
dashboard |
On the client's dashboard deprecated |
object.card.head.panels |
In the header of the limeobject card deprecated |
Migration strategy¶
When migrating from the deprecated slots, use the following strategies.
dashboard
- use the new "Start Page designer" in the administrator interfaceobject.card.head.panels
- This slot has been split into two. Action-like components consisting mainly of a button opening a dialog etc, should be moved to the action menu for the card. Graphical components that displays information should be moved to the "Web components" configuration section for the card view. Both are accessed from the administrator interface of the webclient.
State¶
The state in the webclient stores all information related to data. It can be limetypes, loaded limeobjects, the current user etc.
Connecting to the state¶
To use the state, a property in a web component must be connected to it. This is done by decorating the property with a state decorator. The example below shows how to connect to the state to get the current logged in user.
export class MyComponent implements LimeWebComponent {
@CurrentUser()
@State()
private user: object;
public render() {
return <p>Hello {this.user.fullname}!</p>;
}
}
Filtering the result with arguments¶
It's also possible to send arguments into the decorator in order to make it more specific. The following example will load all loaded limeobjects into the connected property.
export class MyComponent implements LimeWebComponent {
@Limeobjects()
@State()
private limeobjects: object;
}
If we only want objects of a specific limetype, we can specify that limetype when decorating the property. The following will give us all currenyly loaded persons
export class MyComponent implements LimeWebComponent {
@Limeobjects({
limetype: "person",
})
@State()
private persons = [];
}
Using a mapping function¶
While this is useful, it can be made even more useful if combined with a mapping function. Let's say we have a web component that is displayed on the company card, and we want a list of all persons connected to that company. First, we can define a filtering function that filters out our persons like this:
function currentPersons(persons = []) {
return persons.filter((person) => person.company === this.context.id);
}
This function takes a list of persons, and returns a new list which only contains persons of the company in the current context.
We can apply this filtering function by specifying it when decorating our property.
export class MyComponent implements LimeWebComponent {
@Limeobjects({
limetype: "person",
map: [currentPersons],
})
@State()
private persons = [];
}
This will map the original result (a list of all persons) to a new result (a filtered list of persons) that is stored in the connected property.
There are also two specific decorators that can be used to get the limetype and limeobject of the current context. Under the hood, these are just implemented with the map functionality used above.
import {
CurrentLimeobject,
CurrentLimetype,
} from "@limetech/lime-web-components-decorators";
export class MyComponent implements LimeWebComponent {
@CurrentLimeobject()
@State()
private limeobject: object;
@CurrentLimetype()
@State()
private limetype: object;
}
Available state decorators¶
- ApplicationName - Name of the current application
- Configs - Configuration
- CurrentUser - Currently logged in user
- Device - Rough indication of how big the viewport is (desktop, tablet, phone)
- Filters - Filters usable with query objects API
- Limeobjects - Loaded limeobjects
- CurrentLimeobject - The limeobject of the current context
- Limetypes - List of limetypes
- CurrentLimetype - The limetype of the current context
- Session - Information regarding the session
Reloading data in the webclient¶
While information is always kept up to date in web components thanks to the state decorators, this is not true for every part of the webclient. In the best of worlds, things like the card and related lists should update automatically when data is saved. While the aim is for this to be true in the future, we are not there yet. We also understand that there is a need to reload data without refreshing the entire page.
While working on getting all the parts of the webclient state aware, we have provided a temporary solution that can be used to manually reload data in the card, activity feed and related lists. The method is deprecated and code should be made backwards compatible by making sure it exists before calling it. When the webclient has been updated to read all its data from the state, this method will not be needed any longer and will be removed.
The example below illustrates how this can be used. Please note that if there are any unsaved changes on the card, these will be lost when invoking this method.
export class MyComponent implements LimeWebComponent {
@Prop()
public context: LimeWebComponentContext;
@Prop()
public platform: LimeWebComponentPlatform;
private async saveData(data: any) {
const http: HttpService = this.platform.get(PlatformServiceName.Http);
const url = `my_addon/my_endpoint/?id=${this.context.id}`;
const objectService: LimeobjectsStateService = this.platform.get(
PlatformServiceName.LimeobjectsState
);
// Call endpoint that will save some data on the object
await http.put(url, data);
// Manually refresh the object in the webclient, if the method is available
if (objectService.reload) {
objectService.reload(this.context.limetype, this.context.id);
}
}
}
Running Web component tests¶
To run your web components tests run
npm run test
in your frontend folder
Linting Web component¶
To lint your web components run
npm run lint
To try to auto fix any linting issues, simply run
npm run lint:fix