From 44d2d242dc60765a015821b7b3153fdfa18d7c00 Mon Sep 17 00:00:00 2001 From: Ravi Shekhar Date: Wed, 18 Dec 2024 18:02:22 -0600 Subject: [PATCH] feat: add getExtensions method to extend zoid component instance (#468) --- src/child/child.js | 4 +- src/component/component.js | 75 +++++++++++----- src/component/validate.js | 8 +- src/parent/parent.js | 10 +-- test/tests/extensions.jsx | 175 +++++++++++++++++++++++++++++++++++++ test/tests/index.js | 1 + 6 files changed, 238 insertions(+), 35 deletions(-) create mode 100644 test/tests/extensions.jsx diff --git a/src/child/child.js b/src/child/child.js index a06c95cb..dfc69b75 100644 --- a/src/child/child.js +++ b/src/child/child.js @@ -96,8 +96,8 @@ export type ChildComponent = {| init: () => ZalgoPromise, |}; -export function childComponent( - options: NormalizedComponentOptionsType +export function childComponent( + options: NormalizedComponentOptionsType ): ChildComponent { const { tag, propsDef, autoResize, allowedParentDomains } = options; diff --git a/src/component/component.js b/src/component/component.js index c86e0601..fa375718 100644 --- a/src/component/component.js +++ b/src/component/component.js @@ -29,6 +29,7 @@ import { import { childComponent, type ChildComponent } from "../child"; import { + type ParentComponent, type RenderOptionsType, type ParentHelpers, parentComponent, @@ -106,9 +107,10 @@ export type ExportsDefinition = | ExportsConfigDefinition | ExportsMapperDefinition; -export type ComponentOptionsType = {| +export type ComponentOptionsType = {| tag: string, + getExtensions?: (parent: ParentComponent) => ExtType, url: string | (({| props: PropsType

|}) => string), domain?: DomainMatcher, bridgeUrl?: string, @@ -155,10 +157,11 @@ type AutoResizeType = {| element?: string, |}; -export type NormalizedComponentOptionsType = {| +export type NormalizedComponentOptionsType = {| tag: string, name: string, + getExtensions: (parent: ParentComponent) => ExtType, url: string | (({| props: PropsType

|}) => string), domain: ?DomainMatcher, bridgeUrl: ?string, @@ -192,12 +195,13 @@ export type NormalizedComponentOptionsType = {| exports: ExportsMapperDefinition, |}; -export type ZoidComponentInstance = {| +export type ZoidComponentInstance = {| + ...ExtType, ...ParentHelpers

, ...X, ...C, isEligible: () => boolean, - clone: () => ZoidComponentInstance, + clone: () => ZoidComponentInstance, render: ( container?: ContainerReferenceType, context?: $Values @@ -210,14 +214,14 @@ export type ZoidComponentInstance = {| |}; // eslint-disable-next-line flowtype/require-exact-type -export type ZoidComponent = { - (props?: PropsInputType

| void): ZoidComponentInstance, +export type ZoidComponent = { + (props?: PropsInputType

| void): ZoidComponentInstance, // eslint-disable-next-line no-undef driver: (string, mixed) => T, isChild: () => boolean, xprops?: PropsType

, canRenderTo: (CrossDomainWindowType) => ZalgoPromise, - instances: $ReadOnlyArray>, + instances: $ReadOnlyArray>, }; const getDefaultAttributes = (): AttributesType => { @@ -240,15 +244,26 @@ const getDefaultDimensions = (): CssDimensionsType => { return {}; }; -function normalizeOptions( - options: ComponentOptionsType -): NormalizedComponentOptionsType { +function getDefaultGetExtensions(): ( + parent: ParentComponent +) => ExtType { + return function getExtensions(): ExtType { + // $FlowFixMe + const ext: ExtType = {}; + return ext; + }; +} + +function normalizeOptions( + options: ComponentOptionsType +): NormalizedComponentOptionsType { const { tag, url, domain, bridgeUrl, props = {}, + getExtensions = getDefaultGetExtensions(), dimensions = getDefaultDimensions(), autoResize = getDefaultAutoResize(), allowedParentDomains = WILDCARD, @@ -329,31 +344,42 @@ function normalizeOptions( eligible, children, exports: xports, + getExtensions, }; } let cleanInstances = cleanup(); const cleanZoid = cleanup(); -export type Component = {| - init: (props?: PropsInputType

| void) => ZoidComponentInstance, - instances: $ReadOnlyArray>, +export type Component = {| + init: ( + props?: PropsInputType

| void + ) => ZoidComponentInstance, + instances: $ReadOnlyArray>, driver: (string, mixed) => mixed, isChild: () => boolean, canRenderTo: (CrossDomainWindowType) => ZalgoPromise, registerChild: () => ?ChildComponent, |}; -export function component( - opts: ComponentOptionsType -): Component { +export function component( + opts: ComponentOptionsType +): Component { if (__DEBUG__) { validateOptions(opts); } const options = normalizeOptions(opts); - const { name, tag, defaultContext, propsDef, eligible, children } = options; + const { + name, + tag, + defaultContext, + propsDef, + eligible, + children, + getExtensions, + } = options; const global = getGlobal(window); const driverCache = {}; @@ -471,7 +497,7 @@ export function component( const init = ( inputProps?: PropsInputType

| void - ): ZoidComponentInstance => { + ): ZoidComponentInstance => { // eslint-disable-next-line prefer-const let instance; @@ -590,6 +616,7 @@ export function component( }; instance = { + ...getExtensions(parent), ...parent.getExports(), ...parent.getHelpers(), ...getChildren(), @@ -663,15 +690,15 @@ export type ComponentDriverType = {| export type ZoidProps

= PropsType

; // eslint-disable-next-line no-undef -export type CreateZoidComponent = ( +export type CreateZoidComponent = ( // eslint-disable-next-line no-undef - options: ComponentOptionsType + options: ComponentOptionsType // eslint-disable-next-line no-undef -) => ZoidComponent; +) => ZoidComponent; -export const create: CreateZoidComponent = ( - options: ComponentOptionsType -): ZoidComponent => { +export const create: CreateZoidComponent = ( + options: ComponentOptionsType +): ZoidComponent => { setupPostRobot(); const comp = component(options); diff --git a/src/component/validate.js b/src/component/validate.js index 98fdf77f..59c251c4 100644 --- a/src/component/validate.js +++ b/src/component/validate.js @@ -6,8 +6,8 @@ import { CONTEXT, PROP_TYPE } from "../constants"; import type { ComponentOptionsType } from "./index"; -function validatepropsDefinitions( - options: ComponentOptionsType +function validatepropsDefinitions( + options: ComponentOptionsType ) { if (options.props && !(typeof options.props === "object")) { throw new Error(`Expected options.props to be an object`); @@ -49,8 +49,8 @@ function validatepropsDefinitions( } // eslint-disable-next-line complexity -export function validateOptions( - options: ?ComponentOptionsType +export function validateOptions( + options: ?ComponentOptionsType ) { // eslint-ignore-line diff --git a/src/parent/parent.js b/src/parent/parent.js index 070c8f9c..815d47f4 100644 --- a/src/parent/parent.js +++ b/src/parent/parent.js @@ -254,7 +254,7 @@ type RenderOptions = {| rerender: Rerender, |}; -type ParentComponent = {| +export type ParentComponent = {| init: () => void, render: (RenderOptions) => ZalgoPromise, getProps: () => PropsType

, @@ -271,19 +271,19 @@ const getDefaultOverrides =

(): ParentDelegateOverrides

=> { return {}; }; -type ParentOptions = {| +type ParentOptions = {| uid: string, - options: NormalizedComponentOptionsType, + options: NormalizedComponentOptionsType, overrides?: ParentDelegateOverrides

, parentWin?: CrossDomainWindowType, |}; -export function parentComponent({ +export function parentComponent({ uid, options, overrides = getDefaultOverrides(), parentWin = window, -}: ParentOptions): ParentComponent { +}: ParentOptions): ParentComponent { const { propsDef, containerTemplate, diff --git a/test/tests/extensions.jsx b/test/tests/extensions.jsx new file mode 100644 index 00000000..96f179cd --- /dev/null +++ b/test/tests/extensions.jsx @@ -0,0 +1,175 @@ +/* @flow */ +/** @jsx node */ + +import { wrapPromise } from "@krakenjs/belter/src"; +import { getParent } from "@krakenjs/cross-domain-utils/src"; + +import { onWindowOpen, getBody } from "../common"; +import { zoid } from "../zoid"; + +describe("zoid getExtensions cases", () => { + it("should render a component with additional properties to the component", () => { + return wrapPromise(({ expect }) => { + window.__component__ = () => { + return zoid.create({ + tag: "test-render-with-extended-properties", + url: () => "mock://www.child.com/base/test/windows/child/index.htm", + domain: "mock://www.child.com", + getExtensions: () => { + return { + testAttribute: "123", + testMethod: () => "result_of_test_method", + }; + }, + }); + }; + + onWindowOpen().then( + expect("onWindowOpen", ({ win }) => { + if (getParent(win) !== window) { + throw new Error(`Expected window parent to be current window`); + } + }) + ); + + const component = window.__component__(); + const instance = component({ + onRendered: expect("onRendered"), + }); + + if (instance.testAttribute !== "123") { + throw new Error( + `Expected test attribute to be "123", but got ${instance.testAttribute}` + ); + } + if (instance.testMethod() !== "result_of_test_method") { + throw new Error( + `Expected test attribute to be "result_of_test_method", but got ${instance.testMethod()}` + ); + } + + return instance.render(getBody()); + }); + }); + + it("should not be able to ovverride default properties of component using getExtensions", () => { + return wrapPromise(({ expect }) => { + window.__component__ = () => { + return zoid.create({ + tag: "test-extended-properties-cannot-override-defaults", + url: () => "mock://www.child.com/base/test/windows/child/index.htm", + domain: "mock://www.child.com", + getExtensions: () => { + return { + render: () => { + throw new Error("The render method should have been ovveriden"); + }, + }; + }, + }); + }; + + onWindowOpen().then( + expect("onWindowOpen", ({ win }) => { + if (getParent(win) !== window) { + throw new Error(`Expected window parent to be current window`); + } + }) + ); + + const component = window.__component__(); + const instance = component({ + onRendered: expect("onRendered"), + }); + + return instance.render(getBody()); + }); + }); + + it("should be able to interact with method supplied to the component during instantiation", () => { + return wrapPromise(({ expect }) => { + window.__component__ = () => { + return zoid.create({ + tag: "test-render-with-extended-method-invokes-callbacks", + url: () => "mock://www.child.com/base/test/windows/child/index.htm", + domain: "mock://www.child.com", + getExtensions: (parent) => { + return { + invokeCalculatePrice: () => { + return parent.getProps().calculatePrice(); + }, + }; + }, + }); + }; + + onWindowOpen().then( + expect("onWindowOpen", ({ win }) => { + if (getParent(win) !== window) { + throw new Error(`Expected window parent to be current window`); + } + }) + ); + + const component = window.__component__(); + const instance = component({ + onRendered: expect("onRendered"), + calculatePrice: () => 1034.56, + }); + + if (instance.invokeCalculatePrice() !== 1034.56) { + throw new Error( + `Expected test attribute to be "1034.56", but got ${instance.invokeCalculatePrice()}` + ); + } + + return instance.render(getBody()); + }); + }); + + it("should be able to interact with updated methods supplied to the component during instantiation", () => { + return wrapPromise(({ expect }) => { + window.__component__ = () => { + return zoid.create({ + tag: "test-render-with-updated-extended-method-invokes-callbacks", + url: () => "mock://www.child.com/base/test/windows/child/index.htm", + domain: "mock://www.child.com", + getExtensions: (parent) => { + return { + invokeCalculatePrice: () => { + return parent.getProps().calculatePrice(); + }, + }; + }, + }); + }; + + onWindowOpen().then( + expect("onWindowOpen", ({ win }) => { + if (getParent(win) !== window) { + throw new Error(`Expected window parent to be current window`); + } + }) + ); + + const component = window.__component__(); + + const instance = component({ + onRendered: expect("onRendered"), + calculatePrice: () => 1034.56, + }); + + instance.updateProps({ + calculatePrice: () => 50.56, + }); + + if (instance.invokeCalculatePrice() !== 50.56) { + throw new Error( + `Expected test attribute to be "50.56", but got ${instance.invokeCalculatePrice()}` + ); + } + + return instance.render(getBody()); + }); + }); +}); diff --git a/test/tests/index.js b/test/tests/index.js index 029947ba..ffb02e8d 100644 --- a/test/tests/index.js +++ b/test/tests/index.js @@ -20,3 +20,4 @@ import "./remove"; import "./rerender"; import "./method"; import "./children"; +import "./extensions";