Retrieving and Displaying Data

One of the primary use cases for custom element client extensions is to retrieve and display data. While intuitive interface design is critical for successful micro frontends, the user experience also depends on the quality of the data presented. Your solution must be able to retrieve data securely, filter it for context-specific subsets, and serve it up for display. With custom element client extensions, you have granular control over how you retrieve and display your application data.

Your solution must be able to retrieve data securely, filter it for context-specific subsets, and serve it up for display.

Clarity's ticketing app requires a micro-frontend architecture. With this approach, they can tailor ticket data to each user’s profile and provide additional filters for an interactive experience. As Clarity needs to manage both data retrieval and presentation, this is a perfect opportunity to use a custom element client extension.

In this lesson, you’ll take a detailed look at the code for retrieving and displaying Clarity’s ticket data. While every solution implementation is different, this example illustrates the granular control and complex functionality you can achieve with custom element client extensions.

To get started, navigate to /liferay-course-frontend-client-extensions/exercises/module-1/react-app/assets/components. Open TicketsList.js in your preferred editor.

Setting Up Imports and Styles

The first order of business is to set up the core libraries and styling that will be used in this custom element.

import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
import '../style.css';
import Ticket from './Ticket';

This section imports React for building the UI, ReactDOM for rendering it, style.css for basic styling, and the ticket component itself from Ticket.js file. These imports combine to create a structured, readable interface for the ticket data.

Securing API Calls

This custom element client extension will employ Liferay APIs to retrieve user and ticket data. To ensure that each request is secured, Liferay provides an x-csrf-token that serves as an authentication mechanism to protect against cross-site request forgery (CSRF) attacks.

const api = async (url, options = {}) => {
	return fetch(window.location.origin + '/' + url, {
		headers: {
			'Content-Type': 'application/json',
			'x-csrf-token': Liferay.authToken,
		},
		...options,
	});
};

The api helper function makes an API request with two essential parts:

  1. It uses window.location.origin as the base URL, which maintains a secure context by ensuring that all requests stay within the Liferay instance.
  2. It adds x-csrf-token to the request headers with the value Liferay.authToken, a globally available authentication token in Liferay. This token indicates that the request is coming from an approved source within the system.

All API calls in the custom element client extension use the api helper function, so data can be securely retrieved without any additional authentication from the user.

Fetching the Current User’s ID

With Liferay’s headless APIs, you can retrieve details about the user that is currently logged in. Clarity wants to display only tickets that are relevant to the current user, so they can leverage the my-user-account endpoint to fetch the current user’s ID.

const getCurrentUserId = async () => {
	const response = await api('o/headless-admin-user/v1.0/my-user-account');
	const data = await response.json();
	return data.id;
};

The getCurrentUserId function uses the api helper function to call the o/headless-admin-user/v1.0/my-user-account API. This endpoint returns the current user’s full profile. The response is converted to JSON and stored in data. The function then returns data.id, which is the user’s unique ID in the Liferay system. The ID value is used later in the custom element code to show only relevant ticket data to the current user.

Structuring Data for Display

The next piece of the custom element code is to define a model for the data that will be displayed with the micro frontend. The Ticket component (Ticket.js file) is critical for translating the JSON data from an API response into a React-compatible format.

const Ticket = React.memo(({ ticket, onToggle, isOpen }) => (
	<div className="ticket-card" onClick={() => onToggle(ticket.id)}>
		<div className="ticket-header">
			<h3 className="ticket-subject">{ticket.subject}</h3>
			<span className={`ticket-status ${ticket.priority?.key}`}>
				{ticket.ticketStatus.name}
			</span>
		</div>
		{isOpen && (
			<div className="ticket-details">
				<p>Description: {ticket.description}</p>
				{ticket.attachment && (
					<a
						href={ticket.attachment.link.href}
						target="_blank"
						rel="noopener noreferrer"
						className="ticket-attachment"
					>
						View Attachment
					</a>
				)}
				{ticket.relatedTickets && ticket.relatedTickets.length > 0 && (
					<div className="related-tickets">
						<h4>Related Tickets</h4>
						{ticket.relatedTickets.map((related) => (
							<div className="related-ticket-item" key={related.id}>
								ID: {related.id} - {related.subject}
							</div>
						))}
					</div>
				)}
			</div>
		)}
	</div>
));

Retrieving Ticket Data

The core business logic of this custom element lies in the TicketsList function. It retrieves ticket data by leveraging the Object API, a headless API designed to interact with custom objects created in Liferay. The Object API is a powerful tool for building applications that leverage Liferay’s data modeling capabilities.

function TicketsList() {
	const [tickets, setTickets] = useState([]);
	const [expandedTicket, setExpandedTicket] = useState(null);
	const [selectedStatus, setSelectedStatus] = useState('open');
	const [isLoading, setIsLoading] = useState(true);
	const [error, setError] = useState(null);

	useEffect(() => {
		const fetchTickets = async () => {
			try {
				setIsLoading(true);
				const userId = await getCurrentUserId();
				const response = await api('o/c/tickets');
				const data = await response.json();
				const userTickets = (data.items || []).filter(
					(ticket) => ticket.creator?.id === userId
				);
				setTickets(userTickets || []);
			} catch (error) {
				console.error('Error fetching tickets:', error);
				setError('Failed to load tickets.');
			} finally {
				setIsLoading(false);
			}
		};

		fetchTickets();
	}, []);

The first section of the TicketsList function employs the React hook useState to define several state variables:

  • tickets: Stores the user tickets returned by Object API call.
  • expandedTicket: Keeps track of the currently expanded ticket, with which you can show or hide a ticket’s details.
  • selectedStatus: Manages the current filter status (e.g., Open, In Progress, etc.). Users can filter their tickets by status.
  • isLoading: Adds a loading buffer message to the display while ticket data is being retrieved.
  • error: Indicates if an error occurred in loading ticket data for display.

The next section on employs the React hook useEffect to define the following logic for the fetchTickets function:

  1. Set isLoading to true.
  2. Call getCurrentUserId and store the value in userId.
  3. Use the api helper function to call the Object API for ticket data(o/c/tickets).
  4. Store the API response in JSON format in data.
  5. Filterdata for the ticket items with creator.id matching userId. Store the resulting subset of tickets created by the current user in userTickets.
  6. Set the state variable tickets with the value of userTickets.
  7. Catch any errors and finally set isLoading to false.

Calling fetchTickets retrieves user-specific ticket data with secure API calls. The data is now ready to be served up to the user interface with secure API calls. The data is now ready to be served up to the user interface.

Filtering and Displaying Ticket Data

The remaining code in the TicketsList function determines how the ticket data is filtered and displayed by the custom element client extension.

const toggleTicket = (ticketId) => {
	setExpandedTicket(expandedTicket === ticketId ? null : ticketId);
};

const filteredTickets = tickets.filter(
	(ticket) => ticket.ticketStatus.key === selectedStatus
);

if (isLoading) {
	return <div className="loading">Loading...</div>;
}

if (error) {
	return <div className="error">{error}</div>;
}

if (filteredTickets.length === 0) {
	return <div className="no-tickets">No tickets available for this status.</div>;
}

return (
	<div className="tickets-container">
		<div className="tabs">
			{['open', 'inProgress'].map((status) => (
				<button
					key={status}
					className={`tab-button ${selectedStatus === status ? 'active' : ''}`}
					onClick={() => setSelectedStatus(status)}
					aria-selected={selectedStatus === status}
				>
					{status.charAt(0).toUpperCase() + status.slice(1)}
				</button>
			))}
		</div>
		<div className="tickets-list">
			{filteredTickets.map((ticket) => (
				<Ticket
					key={ticket.id}
					ticket={ticket}
					onToggle={toggleTicket}
					isOpen={expandedTicket === ticket.id}
				/>
			))}
		</div>
	</div>
);

The toggleTicket function manages which ticket’s details are expanded or collapsed. The filteredTickets array uses the selectedStatus state to display only tickets that match the currently selected status. After defining messages for loading, error, or empty list, the TicketsList function returns the formatted data.

The remaining code in index.js sets up the CustomElement class, which contains logic for rendering and cleaning up the TicketsList component with ReactDOM.

Receiving Data

In addition to retrieving and displaying data with custom element client extensions, you can also configure custom elements to receive data from users. In other words, you can use custom elements to create forms. Your applications may often require specialized forms that map data fields to custom objects.

While Liferay enables you to create form fragments, you can go a step further and truly unlock yourself by creating custom forms with custom element client extensions. As with all client extensions, you are decoupled from the Liferay container and free to use the technologies of your choice.

Conclusion

Custom element client extensions provide you with granular control over how you retrieve and display data in your micro frontend solutions. With these client extensions, you can securely communicate with other Liferay capabilities, such as objects, using the headless API. You can also use standard web component technologies like React to develop your micro frontends without being tied to Liferay’s portal container or its native user interface. In this lesson, you took an in-depth look at the code for Clarity’s ticket list custom element. Next, you’ll deploy the client extension and see it in action.

Loading Knowledge

Capabilities

Product

Education

Contact Us

Connect

Powered by Liferay
© 2024 Liferay Inc. All Rights Reserved • Privacy Policy