studio-components-embed-code-modal
Usage
To build
npm run build
Development
To run this module for development clone the repo and run
npm install
npm run demo
Sign in to Studio with a Studio UI (Marketing Studio) enabled account. If necessary, click the Profile icon and then the Switch to Studio link.
Navigate to https://studio.brightcove.com/products/videocloud/media/dev/sdk:remote.
The App runs at https://localhost.brightcove.com:12345/js/app.js as described in Creating a Module
The results of the API call to retrieve players will depend on sign in permissions.
Integrating with modules
This component was built for Marketing Studio modules, such as: studio-media-module-four.
Below you will find instructions on how to use the component inside of these modules.
Install the package
npm i @brightcove/components-embed-code-modal
Import the component and required types
import {
EmbedCodeModal,
VideoPayload,
} from '@brightcove/components-embed-code-modal';
VideoPayload is a TypeScript interface which describes an Object that will be accepted as a prop for your <EmbedCodeModal> component.
Here is an overview of the VideoPayload interface:
videoId: string; // Video id, required
videoName: string; // Video title, required
accountId: string; // BC account id, required
posterUrl?: string; // Video poster url, not required
duration: number; // Video duration in seconds, required
Component Props
The component currently takes in two props:
language?: 'en' | 'es' | 'fr' | 'ko' | 'ja' | 'zh';
video: VideoPayload;
- The
VideoPayloadinterface is exported by the component. - The
languageprop is optional, if not passed, the component will get the language fromStudioModule.config.lang. Currently, we use this for testing in our local sandbox.
Wiring Up Through the Module
The modal will only render once the video prop has been passed into the EmbedCodeModal component.
In order to keep our components fully decoupled, we look to our parent module's for wiring up our components together.
Here's an example of how the the Media Module's VideoLibrary is wiring up two separate components in order to open the EmbedCodeModal:
const [embedVideo, setEmbedVideo] = useState<VideoPayload>(null);
const handleAction = (event: DataGridAction) => {
if (event.type === DataGridEvents.GetCode) {
// build VideoPayload for EmbedCodeModal component here
// this way we are completely de-coupling the EmbedCodeModal from the module
const { payload: video } = event;
setEmbedVideo({
videoId: video.id,
videoName: video.name,
accountId: video.account_id,
posterUrl: video?.images?.poster?.src,
duration: video.duration,
});
}
};
- The Media Module imports both the
DataGrid&EmbedCodeModalcomponents. Neither component are coded to be aware of the other. - The
DataGridcomponent dispatches theGetCodeaction to tell its parent module: I would like to retrieve some code, go and do that, however your module handles this action. - The Media Module handles this action by creating a new payload with the video information handed off by the DataGrid. It knows that it wants to open the EmbedCodeModal, so it will look to the
VideoPayloadinterface to create what it needs. - This video object is set to the module's local state.
- Within our JSX, our EmbedCodeModal should be passing this video object to the component:
<EmbedCodeModal video={embedVideo} /> - Anytime that
embedVideoupdates in the module - it triggers the Embed Code Modal to open.
The reasons we have wired things up this way has been to achieve our goals of keeping micro-frontends decoupled. The EmbedCodeModal & the DataGrid are not interacting directly with each other. The DataGrid is providing video data that is being used, but when it triggers a Get Code event, it can just send an entire Video Object along with it. It doesn't need to know exactly what information the Embed Code Modal needs in order to successfully operate.
Likewise, the Embed Code Modal could be triggered by any other number of interfaces, and this abstraction helps us better prepare for the integrations coming in our future.
SPA entry (@brightcove/components-embed-code-modal/spa)
Studio SPA documents load no module-core, so window.StudioModule does not
exist there. The ./spa entry exposes the LIVE and PROJECT surfaces, and takes
every capability from the caller instead of a global:
import {
SpaEmbedCodeModal,
EmbedCodeModalTypes,
} from '@brightcove/components-embed-code-modal/spa';
import type {
LivePayload,
EmbedCodeModalRuntime,
} from '@brightcove/components-embed-code-modal/spa';
<SpaEmbedCodeModal
data={livePayload}
embedCodeModalType={EmbedCodeModalTypes.LIVE}
runtime={{
api: {
config: { publisherId, api }, // the module API config
execute: (descriptor, options) => studioApiCall(descriptor, options),
},
environment, // session.product.env — decides the player origins
locale, // seeds the package-private i18next instance
favoritePlayerId, // optional; absent falls back to 'default'
onNotify, // optional; { variant, title?, message }
shortenUrl, // cookie-authenticated /shortener/shorten adapter
}}
/>;
The PROJECT surface renders the legacy project embed modal (Blue Steel presentation, not prism-core) and takes the same runtime plus three required capabilities:
<SpaEmbedCodeModal
embedCodeModalType={EmbedCodeModalTypes.PROJECT}
data={projectPayload}
runtime={{
api, // as above
environment,
locale,
favoritePlayerId,
shortenUrl,
hasPublishingRole, // getCurrentUserAccount().roles includes PUBLISHING
playersModuleUrl, // e.g. `${product.path}/players`
onNotify, // required: creation failures and copy feedback
}}
/>;
A PROJECT mount fetches the players itself and only offers the players whose
master-branch configuration carries the interactivity plugin. When none does,
it shows the create-player flow if hasPublishingRole is set, and the
activation copy linking to playersModuleUrl otherwise. Creating posts the
interactivity player and refetches the list, and it never publishes.
SpaEmbedCodeModal accepts SpaEmbedCodeModalEntryProps, the union of the LIVE
and PROJECT props. SpaEmbedCodeModalProps remains the LIVE props, so an
existing Omit<SpaEmbedCodeModalProps, 'runtime'> keeps every LIVE key: Omit
over a union collapses to the keys its members share.
What the entry owns and what it does not:
- Owns the players query (a per-mount react-query v3 client with the legacy
retry: 3/staleTime: 0semantics), the i18next instance (six catalogues, registered under theembed-code-modalnamespace only) and the CSS (scoped under.embed-code-modalat build time). - Never reads
window.StudioModule,window.brightcove, the default i18next singleton, or the shell's query client. Two mounted instances with different runtimes cannot share configuration, players or callbacks. - The legacy
EmbedCodeModalexport is unchanged and still reads the Studio document. The two entries are separate webpack outputs (build/index.jsandspa.js), so the legacy-only modules are absent from the SPA file rather than merely unused.npm run buildfails if that isolation regresses. The PROJECT surface shares its presentation with the legacy root throughProjectModalView, and@brightcove/studio-componentsis required by the SPA bundle rather than banned: it is already an external peer, so it adds no bytes.
spa.js, spa.js.map and build/ are build outputs; they are produced by
npm run build and shipped in the published package.
data must be a stable reference: a new object reopens the modal, so an inline
object literal on every render makes a closed modal reopen immediately.
Peer dependency note
The SPA entry keeps react-query v3 at runtime (per-instance client), which is
why react-query remains a peer dependency of this package, as it is for the
legacy entry. A consumer of ./spa must have react-query@^3.38.0 installed.