How to use the Liferay global JS object for your local component JavaScript or TypeScript projects.
At Liferay DevCon today, someone asked me a great question:
“How do you develop and test a local custom element (a standard Web Component) while still using the Liferay JS global object?”
We talked it through, and I hinted I’d get a blog post up about it. Since I’m sure others will hit this same challenge… here we go.
Good news: it’s easier than you might think.
All you really need is a lightweight stub of the Liferay object (just enough of the API surface your custom element relies on) and then import that during local development.
Below is the simple Liferay.js file I use:
const Liferay = window.Liferay || {
Language: {
get: (key) => {
return key;
},
},
OAuth2: {
getAuthorizeURL: () => '',
getBuiltInRedirectURL: () => '',
getIntrospectURL: () => '',
getTokenURL: () => '',
getUserAgentApplication: (_serviceName) => {},
},
OAuth2Client: {
FromParameters: (_options) => {
return {};
},
FromUserAgentApplication: (_userAgentApplicationId) => {
return {};
},
fetch: (_url, _options = {}) => {},
},
ThemeDisplay: {
getCompanyGroupId: () => 20119,
getPathThemeImages: () => '',
getPortalURL: () => 'http://localhost:8080',
getScopeGroupId: () => 20117,
getSiteGroupId: () => 20117,
isSignedIn: () => false,
},
authToken: '',
on: (_event, _callback) => {},
fire: (_event, _data) => {},
};
export default Liferay;
This is not the complete Liferay global object; Liferay’s real runtime includes many more fields and service APIs, but this stub is perfect for local development and testing for the component I was working on at the time.
I included only the pieces I needed:
Liferay.Language.get()Liferay.ThemeDisplayfields for companyId, groupId, portal URL, etc.Liferay.OAuth2+Liferay.OAuth2Client- Event helpers (
on,fire) authToken
If tomorrow I needed something like Liferay.Session.extend(), I’d just add another stub to the same file.
TypeScript Version
If you’re using TypeScript, here’s a version you can drop into something like liferay.ts:
interface LiferayType {
Language: {
get: (key: string) => string;
};
OAuth2: {
getAuthorizeURL: () => string;
getBuiltInRedirectURL: () => string;
getIntrospectURL: () => string;
getTokenURL: () => string;
getUserAgentApplication: (serviceName: string) => unknown;
};
OAuth2Client: {
FromParameters: (options: Record<string, unknown>) =>
unknown;
FromUserAgentApplication: (userAgentApplicationId: string)
=> unknown;
fetch: (url: string, options?: Record<string, unknown>) =>
unknown;
};
ThemeDisplay: {
getCompanyGroupId: () => number;
getPathThemeImages: () => string;
getPortalURL: () => string;
getScopeGroupId: () => number;
getSiteGroupId: () => number;
isSignedIn: () => boolean;
};
authToken: string;
on: (event: string, callback: (...args: unknown[]) => void)
=> void;
fire: (event: string, data?: unknown) => void;
}
const Liferay: LiferayType =
(window as any).Liferay ||
({
Language: {
get: (key: string) => key,
},
OAuth2: {
getAuthorizeURL: () => '',
getBuiltInRedirectURL: () => '',
getIntrospectURL: () => '',
getTokenURL: () => '',
getUserAgentApplication: (_serviceName: string) => {},
},
OAuth2Client: {
FromParameters: (_options: Record<string, unknown>) =>
({}),
FromUserAgentApplication: (_id: string) => ({}),
fetch: (_url: string, _options?: Record<string, unknown>)
=> {},
},
ThemeDisplay: {
getCompanyGroupId: () => 20119,
getPathThemeImages: () => '',
getPortalURL: () => 'http://localhost:8080',
getScopeGroupId: () => 20117,
getSiteGroupId: () => 20117,
isSignedIn: () => false,
},
authToken: '',
on: (_event: string, _callback: (...args: unknown[]) =>
void) => {},
fire: (_event: string, _data?: unknown) => {},
} as LiferayType);
export default Liferay;
You'll find this one to be very useful since it provides the type-safe definition of the runtime object that Liferay doesn't provide otherwise.
Adding missing stubs to the TypeScript version does take slightly more effort here.
Because TypeScript enforces structure, you’ll want to make sure your interface matches the real Liferay global object, or at least the parts of it your component actually uses. That means:
- Matching method signatures (e.g., what parameters
Liferay.OAuth2Client.fetch()expects) - Matching return types
- Including any nested services or namespaces your component depends on
If you get it wrong, your component may type-check locally but throw errors once deployed into Liferay.
So where do you find the real definitions? The easiest way I've found is to use the Browser console: Open DevTools → type Liferay → expand it. I know it seems kind of odd, but when you see the Liferay source, the JS is scattered around and merged at runtime, so it can be hard to find the pieces that you need in there directly.
You don’t need to replicate the entire object, just enough for your component to run locally in a type-safe way.
Wrapping Up
This small pattern, a simple Liferay.js (or liferay.ts) stub, has been incredibly useful for me whenever I'm building local custom elements that rely on the Liferay global object.
It lets me:
- Develop and test locally at full speed
- Avoid spinning up a portal every time
- Use the
||syntax so my stub works locally but automatically defers to the realwindow.Liferaywhen deployed
Short and sweet, but very practical.
If you’ve ever struggled with local development for Liferay-integrated custom elements, I hope this makes your day just a bit easier.
