Skip to content

Commit

Permalink
WIP - clean up XBlock rendering context, add handler functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
bradenmacdonald committed May 31, 2024
1 parent 3e4e3a3 commit d13781a
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 47 deletions.
29 changes: 28 additions & 1 deletion public/static/xblock/client-v0.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ function toVdom(element, nodeName) {
return _$1(nodeName || element.nodeName.toLowerCase(), props, wrappedChildren);
}

/**
* Preact hook to make it easy to access the XBlock field data from the Web Component implementation
*
* e.g. `const { learnerAnswer } = useFields(props);`
*/
function useFields(props) {
var parts = [props['system-fields'], props['content-fields'], props['user-fields']];
return q(function () {
Expand All @@ -183,8 +188,30 @@ function useFields(props) {
return fields;
}, parts);
}
/**
* Get the DOM element (web component) for this XBlock.
* @param usageKey The usage key, available as an attribute/prop on the web component.
*/
function getDomElement(usageKey) {
var result = document.querySelector(".xblock-component.xblock-v2[usage-key=\"".concat(usageKey, "\"]"));
if (result === null) {
throw new Error("Unable to find DOM element for ".concat(usageKey));
}
return result;
}
function getRenderContext(usageKey) {
var result = getDomElement(usageKey).closest('xblock-render-context');
if (result === null) {
throw new Error("XBlock was rendered outside of an <xblock-render-context>: ".concat(usageKey));
}
return result;
}
function callHandler(usageKey, handlerName, body, method) {
if (method === void 0) { method = 'POST'; }
return getRenderContext(usageKey).callHandler(usageKey, handlerName, body, method);
}
function registerPreactXBlock(componentClass, blockType, options) {
register(componentClass, "xblock2-".concat(blockType), ['content-fields', 'user-fields'], options);
}

export { b$1 as Component, k$1 as Fragment, E as cloneElement, G as createContext, _$1 as createElement, m$2 as createRef, _$1 as h, m as html, D$1 as hydrate, t$2 as isValidElement, l$1 as options, registerPreactXBlock, B$1 as render, H as toChildArray, x as useCallback, P as useContext, V as useDebugValue, _ as useEffect, b as useErrorBoundary, useFields, g as useId, T as useImperativeHandle, A as useLayoutEffect, q as useMemo, y as useReducer, F as useRef, p as useState };
export { b$1 as Component, k$1 as Fragment, callHandler, E as cloneElement, G as createContext, _$1 as createElement, m$2 as createRef, getDomElement, _$1 as h, m as html, D$1 as hydrate, t$2 as isValidElement, l$1 as options, registerPreactXBlock, B$1 as render, H as toChildArray, x as useCallback, P as useContext, V as useDebugValue, _ as useEffect, b as useErrorBoundary, useFields, g as useId, T as useImperativeHandle, A as useLayoutEffect, q as useMemo, y as useReducer, F as useRef, p as useState };
46 changes: 3 additions & 43 deletions src/courseware/component/XBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,23 @@
import React from "react";
import { getConfig } from "@edx/frontend-platform";
import { Helmet } from 'react-helmet';

import { XBlockData, XBlockDataV2 } from "./types";
import LegacyXBlock from "./LegacyXBlock";

interface XBlockRenderContextData {
ensureBlockScript(blockType: string);
}

const XBlockRenderContext = React.createContext<XBlockRenderContextData|undefined>(undefined);



export const XBlockRenderingContext: React.FC<{children: React.ReactNode}> = ({children}) => {
const globalStatus = (window as any as {_loadedBlockTypes: Set<string>});
if (!globalStatus._loadedBlockTypes) {
globalStatus._loadedBlockTypes = new Set<string>();
}
const ensureBlockScript = React.useCallback((blockType: string) => {
if (!globalStatus._loadedBlockTypes.has(blockType)) {
globalStatus._loadedBlockTypes.add(blockType);
// We want the browser to handle this import(), not webpack, so the comment on the next line is essential.
import(/* webpackIgnore: true */ `${getConfig().LMS_BASE_URL}/xblock/resource-v2/${blockType}/public/learner-view-v2.js`).then(() => {
console.log(`Loaded JavaScript for ${blockType} v2 XBlock.`);
}, (err) => {
console.error(`🛑 Unable to Load JavaScript for ${blockType} v2 XBlock: ${err}`);
globalStatus._loadedBlockTypes.delete(blockType);
});
}
}, []);
const ctx = {
ensureBlockScript,
};
return <XBlockRenderContext.Provider value={ctx}>
{children}
</XBlockRenderContext.Provider>;
};


import { ensureBlockScript } from "./XBlockRenderContext";


export const XBlock: React.FC<XBlockDataV2> = ({ id, ...props }) => {

const ctx = React.useContext(XBlockRenderContext);
if (!ctx) {
return <p>Error: cannot display a v2 XBlock outside of an <code>XBlockRenderContext</code>.</p>
}

const ComponentName = `xblock2-${props.blockType}`;
const xblockProps = {
class: 'xblock-component xblock-v2', // For web components in React, the prop is 'class', not 'className'
'usage-key': id,
'content-fields': JSON.stringify(props.contentFields),
'system-fields': JSON.stringify(props.systemFields),
'user-fields': JSON.stringify(props.userFields),
};

React.useEffect(() => {
ctx.ensureBlockScript(props.blockType);
ensureBlockScript(props.blockType);
});

// return React.createElement(componentName, xblockProps);
Expand Down
64 changes: 64 additions & 0 deletions src/courseware/component/XBlockRenderContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from "react";
import type { AxiosInstance } from "axios";
import { getConfig } from "@edx/frontend-platform";
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

const globalStatus = (window as any as {_loadedBlockTypes: Set<string>});
if (!globalStatus._loadedBlockTypes) {
globalStatus._loadedBlockTypes = new Set<string>();
}

// TODO: HandlerResponse and CallHandler type definitions should be in a 'xblock-client-api' package on NPM?
export interface HandlerResponse {
data: Record<string, any>;
updatedFields: Record<string, any>;
}

export interface CallHandler {
(
usageKey: string,
handlerName: string,
body: Record<string, any> | ReadableStream | Blob,
method?: 'GET'|'POST',
): Promise<HandlerResponse>;
}

export function ensureBlockScript(blockType: string): void {
if (!globalStatus._loadedBlockTypes.has(blockType)) {
globalStatus._loadedBlockTypes.add(blockType);
// We want the browser to handle this import(), not webpack, so the comment on the next line is essential.
import(/* webpackIgnore: true */ `${getConfig().LMS_BASE_URL}/xblock/resource-v2/${blockType}/public/learner-view-v2.js`).then(() => {
console.log(`Loaded JavaScript for ${blockType} v2 XBlock.`);
}, (err) => {
console.error(`🛑 Unable to Load JavaScript for ${blockType} v2 XBlock: ${err}`);
globalStatus._loadedBlockTypes.delete(blockType);
});
}
}

const callHandler: CallHandler = async (usageKey, handlerName, body, method): Promise<HandlerResponse> => {
const client: AxiosInstance = getAuthenticatedHttpClient();
const makeRequest = client[method === "POST" ? 'post' : 'get'];
const response = await makeRequest(`${getConfig().LMS_BASE_URL}/api/xblock/v1/${usageKey}/handler_v2/${handlerName}`, body);
return response.data;
}

/**
* Context Provider for rendering XBlocks (v2). Any use of the <XBlock /> component must be inside one of these
* contexts. This provides the functionality for those XBlocks to interact with the runtime, e.g. by calling handlers.
*/
export const XBlockRenderContextProvider: React.FC<{children: React.ReactNode}> = ({children}) => {
// Place an actual element into the DOM so that descendants can find us even if they're not using React
const ContextElementType = "xblock-render-context" as any as React.FC<{ref: any}>;
const ref = React.useRef<HTMLElement>(null);
React.useEffect(() => {
if (ref.current) {
(ref.current as any).callHandler = callHandler;
}
}, [ref.current]);
return (
<ContextElementType ref={ref}>
{children}
</ContextElementType>
);
};
7 changes: 4 additions & 3 deletions src/courseware/course/sequence/Unit/UnitContent.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from "react";
import { useUnitContents } from "./data/apiHooks";
import { Spinner } from "@openedx/paragon";
import { AutoXBlock, XBlockRenderingContext } from "../../../component/XBlock";
import { AutoXBlock } from "../../../component/XBlock";
import { XBlockRenderContextProvider } from "../../../component/XBlockRenderContext";


interface Props {
Expand All @@ -26,11 +27,11 @@ export const UnitContent: React.FC<Props> = ({ unitId, ...props }) => {
}

return <main>
<XBlockRenderingContext>
<XBlockRenderContextProvider>
{unitContents.blocks.map((blockData) => (
<AutoXBlock key={blockData.id} {...blockData} />
))}
</XBlockRenderingContext>
</XBlockRenderContextProvider>
</main>;
};

Expand Down

0 comments on commit d13781a

Please sign in to comment.