dile-many-relation
The dile-many-relation component provides a generic front-end interface for managing many-to-many relationships. It displays the currently associated items and allows users to add new ones via a searchable select or remove existing ones, all backed by REST API endpoints.
It is designed to work with any pair of entities. A typical use case would be managing the Tags associated with a Board Game from the Board Game detail page.
Installation
npm i @dile/crud
Usage
Import the component:
import '@dile/crud/components/many-relation/many-relation.js';
Basic usage:
<dile-many-relation
endpointGet="/api/tags"
endpointList="/api/board-games/1/tags"
endpointAdd="/api/board-games/1/tags"
endpointRemove="/api/board-games/1/tags"
idProperty="id"
bodyIdProperty="tag_id"
displayProperty="name"
resultDataProperty="data"
loadFromEndpoint
language="en"
></dile-many-relation>
Properties
Endpoints
- endpointGet: String. API endpoint used by the internal select to search all available items (e.g.
/api/tags). - endpointList: String. API endpoint to retrieve the currently related items (e.g.
/api/board-games/1/tags). Used on initial load whenloadFromEndpointis true, and after every add or remove operation to refresh the list. - endpointAdd: String. API endpoint for the POST request that adds a new relation (e.g.
/api/board-games/1/tags). - endpointRemove: String. Base API endpoint for the DELETE request. The related item's ID is appended automatically (e.g.
/api/board-games/1/tags→DELETE /api/board-games/1/tags/6).
Initial data
- relatedItems: Array. Currently related items passed in from the parent. Used when
loadFromEndpointisfalse. Defaults to[]. - loadFromEndpoint: Boolean. When
true, the component ignoresrelatedItemsand fetches the initial list fromendpointListon mount. After every add or remove operation the list is always refreshed fromendpointList, using the API as the source of truth. Defaults tofalse.
Select configuration (passed to the internal dile-ajax-select-crud)
- displayProperty: String. Property of each item to display as its label in the select. Defaults to
'name'. - idProperty: String. Property of each item used as its unique identifier. Defaults to
'id'. - bodyIdProperty: String. Key used in the POST body when adding a relation. Useful when the POST body key differs from the item's ID property (e.g.
tag_idinstead ofid). Defaults to the value ofidProperty. - queryStringVariable: String. Query string parameter name sent to
endpointGetwhen the user types in the search field. - placeholder: String. Placeholder text for the search input inside the select.
- selectDefaultPlaceholder: String. Placeholder shown in the select dropdown when no item is selected. Defaults to the i18n value (
"Select..."/"Selecciona..."). - resultDataProperty: String. Property path in the
endpointGetresponse that contains the array of items for the select (e.g.'data'). - maxResults: Number. Maximum number of results requested per search query. Must be set together with
pageParamName— if only one of the two is provided, neither has any effect. - pageParamName: String. Query string parameter name used to pass the page size to
endpointGet. Must be set together withmaxResults— if only one of the two is provided, neither has any effect. - getSelectResultList: Object (Function). Custom function to extract the items array from the
endpointGetresponse. Signature:(response) => [].
List customisation
- label: String. Optional heading label displayed above the select input. When not set, no label is rendered.
- itemTemplate: Object (Function). Function used to render each related item in the list. Receives the item object and must return a lit-html template. Defaults to rendering
item[displayProperty]. Example:.itemTemplate=${item => html\${item.name}`}`. - getListItems: Object (Function). Function to extract the items array from the
endpointListresponse. Defaults to(response) => response.data. - listIcon: Object. Optional icon object to display before each related item in the list. When not set, no icon is rendered. Use with an icon from
@dile/icons. Example:.listIcon=${myIcon}. - emptyListMessage: String. Custom message shown when there are no related items. If not set, the i18n value is used (
"There are no items yet"/"No tenemos elementos todavía"). - addRelationLabel: String. Override the label of the add button shown as a tooltip. If not set, the i18n value is used (
"Add"/"Añadir"). - language: String. Language code for the i18n system (
'en','es'). Propagated from the parent. - message: String. Feedback message displayed below the component using
dile-input-message. - errored: Boolean. When
true, the message is styled as an error. Requiresmessageto be set. - hideErrorOnInput: Boolean. When
true, clearsmessageanderroredautomatically once a relation has been successfully added (i.e., after the POST completes).
Custom events
- many-relation-add-success: Dispatched when a new relation has been successfully created via POST. The
detailproperty contains the server response. - many-relation-add-error: Dispatched when the POST request to add a relation fails. The
detailproperty contains the server response. - many-relation-remove-success: Dispatched when a relation has been successfully deleted via DELETE.
- many-relation-remove-error: Dispatched when the DELETE request to remove a relation fails.
- many-relation-add-no-selection: Dispatched when the user clicks the add button without having selected an item first. Use this to show a warning message prompting the user to select an item before adding.
- many-relation-list-error: Dispatched when the GET request to refresh the related items list fails.
Styling
| Custom property | Description | Default |
|---|---|---|
--dile-input-label-font-size |
Font size of the label heading | 1em |
--dile-input-label-color |
Color of the label heading | #59e |
--dile-input-label-font-weight |
Font weight of the label heading | normal |
--dile-input-label-margin-bottom |
Bottom margin of the label heading | 4px |
--many-relation-item-font-size |
Font size of each related item label | 0.9rem |
--many-relation-item-color |
Text color of each related item label | inherit |
--many-relation-item-font-weight |
Font weight of each related item label | normal |
--many-relation-item-gap |
Gap between elements in each related item | 0.5rem |
--many-relation-item-content-padding-left |
Left padding of each related item label | 0.4rem |
--many-relation-list-icon-size |
Size of the optional list icon before each item | 24px |
--many-relation-list-icon-color |
Color of the optional list icon before each item | #666 |
--many-relation-add-size |
Size of the add icon button (also controls the width of all icon columns for alignment) | 38px |
--many-relation-add-color |
Color of the add icon | #2962ff |
--many-relation-add-disabled-color |
Color of the add icon when no item is selected | #ccc |
--many-relation-remove-size |
Size of the remove icon | 24px |
--many-relation-remove-color |
Color of the remove icon | #c00 |
--many-relation-empty-font-size |
Font size of the empty list message | 0.9rem |
--many-relation-empty-color |
Text color of the empty list message | #888 |
--many-relation-empty-font-style |
Font style of the empty list message | italic |
The component also uses dile-ajax-select-crud, dile-icon, and dile-spinner internally, so their respective CSS custom properties are also available.
dile-many-relation Demo
<script type="module">
import { LitElement, html, css } from 'lit';
import '@dile/crud/components/many-relation/many-relation.js';
import '@dile/crud/components/ajax-select-crud/ajax-select-crud.js';
import '@dile/ui/components/form-separator/form-separator.js';
export class DemoManyRelation extends LitElement {
static styles = [
css`
:host {
display: block;
}
`
];
static get properties() {
return {
gameId: { type: Number },
};
}
constructor() {
super();
this.gameId = 1;
}
render() {
return html`
<dile-ajax-select-crud
idProperty="id"
name="game_id"
label="Board Game"
endpoint="https://timer.escuelait.com/api/board-games"
queryStringVariable="search"
value="${this.gameId}"
placeholder="Search board game..."
.getSelectResultList=${(response) => response.data.result.data}
displayProperty="name"
selectDefaultPlaceholder="Select board game..."
@element-changed=${this.onGameChanged}
></dile-ajax-select-crud>
${this.gameId
? this.relationTemplate
: 'Select board game...'
}
`;
}
get relationTemplate() {
return html`
<dile-form-separator label="dile-many-relation component in action"></dile-form-separator>
<dile-many-relation
endpointGet="https://timer.escuelait.com/api/tags"
endpointList="https://timer.escuelait.com/api/board-games/${this.gameId}/tags"
endpointAdd="https://timer.escuelait.com/api/board-games/${this.gameId}/tags"
endpointRemove="https://timer.escuelait.com/api/board-games/${this.gameId}/tags"
idProperty="id"
bodyIdProperty="tag_id"
displayProperty="name"
resultDataProperty="data"
loadFromEndpoint
language="en"
.itemTemplate=${(item) => html`<b>${item.name}</b>`}
></dile-many-relation>
`
}
onGameChanged(e) {
this.gameId = e.detail.value;
}
}
customElements.define('demo-many-relation', DemoManyRelation);
</script>
<demo-many-relation></demo-many-relation>
dile-components