From f9c8cadcd18c924eeafdefa8a1c9346968dfe40a Mon Sep 17 00:00:00 2001 From: Daniel Brain Date: Tue, 7 Dec 2021 13:39:24 -0800 Subject: [PATCH] Support passing container to component prop helper functions (#382) * Pass container to prop definitions when available (#376) * Avoid re-decorating the same prop multiple times * Only use node 14 for github ci --- docs/api/prop-definitions.md | 357 ++++++++++++++++++++--------------- src/component/component.js | 8 +- src/component/props.js | 22 +-- src/parent/parent.js | 73 ++++--- src/parent/props.js | 139 ++++++-------- src/types.js | 2 + test/tests/props.js | 142 +++++++++++++- 7 files changed, 460 insertions(+), 283 deletions(-) diff --git a/docs/api/prop-definitions.md b/docs/api/prop-definitions.md index f447a477..21067e29 100644 --- a/docs/api/prop-definitions.md +++ b/docs/api/prop-definitions.md @@ -1,156 +1,205 @@ # Prop Definitions -## type `string` - - The data-type expected for the prop - - - `'string'` - - `'number'` - - `'boolean'` - - `'object'` - - `'function'` - - `'array'` - -## required `boolean` - - Whether or not the prop is mandatory. Defaults to `true`. - - ```javascript - onLogin: { - type: 'function', - required: false - } - ``` - -## default `({ props }) => value` - - A function returning the default value for the prop, if none is passed - - ```javascript - email: { - type: 'string', - required: false, - default: function() { - return 'a@b.com'; - } - } - ``` - -## validate `({ value, props }) => void` - - A function to validate the passed value. Should throw an appropriate error if invalid. - - ```javascript - email: { - type: 'string', - validate: function({ value, props }) { - if (!value.match(/^.+@.+\..+$/)) { - throw new TypeError(`Expected email to be valid format`); - } - } - } - ``` - -## queryParam `boolean | string | (value) => string` - - Should a prop be passed in the url. - - ```javascript - email: { - type: 'string', - queryParam: true // ?email=foo@bar.com - } - ``` - - If a string is set, this specifies the url param name which will be used. - - ```javascript - email: { - type: 'string', - queryParam: 'user-email' // ?user-email=foo@bar.com - } - ``` - - If a function is set, this is called to determine the url param which should be used. - - ```javascript - email: { - type: 'string', - queryParam: function({ value }) { - if (value.indexOf('@foo.com') !== -1) { - return 'foo-email'; // ?foo-email=person@foo.com - } else { - return 'generic-email'; // ?generic-email=person@whatever.com - } - } - } - ``` - -## value `({ props }) => value` - - The value for the prop, if it should be statically defined at component creation time - - ```javascript - userAgent: { - type: 'string', - value() { - return window.navigator.userAgent; - } - } - ``` - -## decorate `({ value, props }) => value` - - A function used to decorate the prop at render-time. Called with the value of the prop, should return the new value. - - ```javascript - onLogin: { - type: 'function', - decorate(original) { - return function() { - console.log('User logged in!'); - return original.apply(this, arguments); - }; - } - } - ``` - -## serialization `string` - - If `json`, the prop will be JSON stringified before being inserted into the url - - ```javascript - user: { - type: 'object', - serialization: 'json' // ?user={"name":"Zippy","age":34} - } - ``` - - If `dotify` the prop will be converted to dot-notation. - - ```javascript - user: { - type: 'object', - serialization: 'dotify' // ?user.name=Zippy&user.age=34 - } - ``` - - If `base64`, the prop will be JSON stringified then base64 encoded before being inserted into the url - - ```javascript - user: { - type: 'object', - serialization: 'base64' // ?user=eyJuYW1lIjoiWmlwcHkiLCJhZ2UiOjM0fQ== - } - ``` - -## alias `string` - - An aliased name for the prop - - ```javascript - onLogin: { - type: 'function', - alias: 'onUserLogin' - } - ``` \ No newline at end of file +## type + +`string` + +The data-type expected for the prop + +- `'string'` +- `'number'` +- `'boolean'` +- `'object'` +- `'function'` +- `'array'` + +## required + +`boolean` + +Whether or not the prop is mandatory. Defaults to `true`. + +```javascript +onLogin: { + type: 'function', + required: false +} +``` + +## default + +```javascript +({ + props : Props, + state : Object, + close : () => Promise, + focus : () => Promise, + onError : (mixed) => Promise, + container : HTMLElement | undefined, + event : Event +}) => value +``` + +A function returning the default value for the prop, if none is passed + +```javascript +email: { + type: 'string', + required: false, + default: function() { + return 'a@b.com'; + } +} +``` + +## validate + +`({ value, props }) => void` + +A function to validate the passed value. Should throw an appropriate error if invalid. + +```javascript +email: { + type: 'string', + validate: function({ value, props }) { + if (!value.match(/^.+@.+\..+$/)) { + throw new TypeError(`Expected email to be valid format`); + } + } +} +``` + +## queryParam + +`boolean | string | (value) => string` + +Should a prop be passed in the url. + +```javascript +email: { + type: 'string', + queryParam: true // ?email=foo@bar.com +} +``` + +If a string is set, this specifies the url param name which will be used. + +```javascript +email: { + type: 'string', + queryParam: 'user-email' // ?user-email=foo@bar.com +} +``` + +If a function is set, this is called to determine the url param which should be used. + +```javascript +email: { + type: 'string', + queryParam: function({ value }) { + if (value.indexOf('@foo.com') !== -1) { + return 'foo-email'; // ?foo-email=person@foo.com + } else { + return 'generic-email'; // ?generic-email=person@whatever.com + } + } +} +``` + +## value + +```javascript +({ + props : Props, + state : Object, + close : () => Promise, + focus : () => Promise, + onError : (mixed) => Promise, + container : HTMLElement | undefined, + event : Event +}) => value +``` + +The value for the prop, if it should be statically defined at component creation time + +```javascript +userAgent: { + type: 'string', + value() { + return window.navigator.userAgent; + } +} +``` + +## decorate + +```javascript +({ + props : Props, + state : Object, + close : () => Promise, + focus : () => Promise, + onError : (mixed) => Promise, + container : HTMLElement | undefined, + event : Event, + value : Value +}) => value +``` + +A function used to decorate the prop at render-time. Called with the value of the prop, should return the new value. + +```javascript +onLogin: { + type: 'function', + decorate(original) { + return function() { + console.log('User logged in!'); + return original.apply(this, arguments); + }; + } +} +``` + +## serialization + +`string` + +If `json`, the prop will be JSON stringified before being inserted into the url + +```javascript +user: { + type: 'object', + serialization: 'json' // ?user={"name":"Zippy","age":34} +} +``` + +If `dotify` the prop will be converted to dot-notation. + +```javascript +user: { + type: 'object', + serialization: 'dotify' // ?user.name=Zippy&user.age=34 +} +``` + +If `base64`, the prop will be JSON stringified then base64 encoded before being inserted into the url + +```javascript +user: { + type: 'object', + serialization: 'base64' // ?user=eyJuYW1lIjoiWmlwcHkiLCJhZ2UiOjM0fQ== +} +``` + +## alias + +`string` + +An aliased name for the prop + +```javascript +onLogin: { + type: 'function', + alias: 'onUserLogin' +} +``` \ No newline at end of file diff --git a/src/component/component.js b/src/component/component.js index 0681c902..4dea2b33 100644 --- a/src/component/component.js +++ b/src/component/component.js @@ -11,7 +11,7 @@ import { type RenderOptionsType, type ParentHelpers, parentComponent } from '../ import { ZOID, CONTEXT, POST_MESSAGE, WILDCARD, METHOD, PROP_TYPE } from '../constants'; import { react, angular, vue, vue3, angular2 } from '../drivers'; import { getGlobal, destroyGlobal, getInitialParentPayload, isChildComponentWindow } from '../lib'; -import type { CssDimensionsType, StringMatcherType } from '../types'; +import type { CssDimensionsType, StringMatcherType, ContainerReferenceType } from '../types'; import { validateOptions } from './validate'; import { defaultContainerTemplate, defaultPrerenderTemplate } from './templates'; @@ -135,8 +135,8 @@ export type ZoidComponentInstance = {| ...C, isEligible : () => boolean, clone : () => ZoidComponentInstance, - render : (container? : string | HTMLElement, context? : $Values) => ZalgoPromise, - renderTo : (target : CrossDomainWindowType, container? : string | HTMLElement, context? : $Values) => ZalgoPromise + render : (container? : ContainerReferenceType, context? : $Values) => ZalgoPromise, + renderTo : (target : CrossDomainWindowType, container? : ContainerReferenceType, context? : $Values) => ZalgoPromise |}; // eslint-disable-next-line flowtype/require-exact-type @@ -335,7 +335,7 @@ export function component(opts : ComponentOptionsType) : Compo }); }; - const getDefaultContainer = (context : $Values, container? : string | HTMLElement) : string | HTMLElement => { + const getDefaultContainer = (context : $Values, container? : ContainerReferenceType) : ContainerReferenceType => { if (container) { if (typeof container !== 'string' && !isElement(container)) { throw new TypeError(`Expected string or element selector to be passed`); diff --git a/src/component/props.js b/src/component/props.js index 4222203f..849746ab 100644 --- a/src/component/props.js +++ b/src/component/props.js @@ -115,6 +115,7 @@ export type PropDefinitionType, X> = {| close : () => ZalgoPromise, focus : () => ZalgoPromise, onError : (mixed) => ZalgoPromise, + container : HTMLElement | void, event : EventEmitterType |}) => ?T, default? : ({| @@ -123,6 +124,7 @@ export type PropDefinitionType, X> = {| close : () => ZalgoPromise, focus : () => ZalgoPromise, onError : (mixed) => ZalgoPromise, + container : HTMLElement | void, event : EventEmitterType |}) => ?T, decorate? : ({| @@ -132,6 +134,7 @@ export type PropDefinitionType, X> = {| close : () => ZalgoPromise, focus : () => ZalgoPromise, onError : (mixed) => ZalgoPromise, + container : HTMLElement | void, event : EventEmitterType |}) => T, childDecorate? : ({| @@ -424,22 +427,19 @@ export function getBuiltInProps() : BuiltInPropsDefinitionType { } type PropCallback = - ((string, BooleanPropDefinitionType, boolean) => R) & - ((string, StringPropDefinitionType, string) => R) & - ((string, NumberPropDefinitionType, number) => R) & - ((string, FunctionPropDefinitionType, Function) => R) & - ((string, ArrayPropDefinitionType<$ReadOnlyArray<*> | $ReadOnlyArray<*>, P, X>, $ReadOnlyArray<*> | $ReadOnlyArray<*>) => R) & - ((string, ObjectPropDefinitionType, Object) => R); + ((string, BooleanPropDefinitionType | void, boolean) => R) & + ((string, StringPropDefinitionType | void, string) => R) & + ((string, NumberPropDefinitionType | void, number) => R) & + ((string, FunctionPropDefinitionType | void, Function) => R) & + ((string, ArrayPropDefinitionType<$ReadOnlyArray<*> | $ReadOnlyArray<*>, P, X> | void, $ReadOnlyArray<*> | $ReadOnlyArray<*>) => R) & + ((string, ObjectPropDefinitionType | void, Object) => R); export function eachProp(props : PropsType

, propsDef : PropsDefinitionType, handler : PropCallback) { - for (const key of Object.keys(props)) { + // $FlowFixMe[cannot-spread-indexer] + for (const key of Object.keys({ ...props, ...propsDef })) { const propDef = propsDef[key]; const value = props[key]; - if (!propDef) { - continue; - } - // $FlowFixMe[incompatible-call] handler(key, propDef, value); } diff --git a/src/parent/parent.js b/src/parent/parent.js index f53ca60a..ad3f3c0b 100644 --- a/src/parent/parent.js +++ b/src/parent/parent.js @@ -9,14 +9,14 @@ import { ZalgoPromise } from 'zalgo-promise/src'; import { addEventListener, uniqueID, elementReady, writeElementToWindow, eventEmitter, type EventEmitterType, noop, onResize, extendUrl, appendChild, cleanup, once, stringifyError, destroyElement, getElementSafe, showElement, hideElement, iframe, memoize, isElementClosed, - awaitFrameWindow, popup, normalizeDimension, watchElementForClose, isShadowElement, insertShadowSlot } from 'belter/src'; + awaitFrameWindow, popup, normalizeDimension, watchElementForClose, isShadowElement, insertShadowSlot, extend } from 'belter/src'; import { ZOID, POST_MESSAGE, CONTEXT, EVENT, METHOD, WINDOW_REFERENCE, DEFAULT_DIMENSIONS } from '../constants'; import { getGlobal, getProxyObject, crossDomainSerialize, buildChildWindowName, type ProxyObject } from '../lib'; import type { PropsInputType, PropsType } from '../component/props'; import type { ChildExportsType } from '../child'; -import type { CssDimensionsType } from '../types'; +import type { CssDimensionsType, ContainerReferenceType } from '../types'; import type { NormalizedComponentOptionsType, AttributesType } from '../component'; import { serializeProps, extendProps } from './props'; @@ -102,7 +102,7 @@ type RenderContainerOptions = {| type ResolveInitPromise = () => ZalgoPromise; type RejectInitPromise = (mixed) => ZalgoPromise; -type GetProxyContainer = (container : string | HTMLElement) => ZalgoPromise>; +type GetProxyContainer = (container : ContainerReferenceType) => ZalgoPromise>; type Show = () => ZalgoPromise; type Hide = () => ZalgoPromise; type Close = () => ZalgoPromise; @@ -159,7 +159,7 @@ type DelegateOverrides = {| type RenderOptions = {| target : CrossDomainWindowType, - container : string | HTMLElement, + container : ContainerReferenceType, context : $Values, rerender : Rerender |}; @@ -195,6 +195,7 @@ export function parentComponent({ uid, options, overrides = getDefaultO const handledErrors = []; const clean = cleanup(); const state = {}; + const inputProps = {}; let internalState = { visible: true }; @@ -205,6 +206,7 @@ export function parentComponent({ uid, options, overrides = getDefaultO let currentProxyContainer : ?ProxyObject; let childComponent : ?ChildExportsType

; let currentChildDomain : ?string; + let currentContainer : HTMLElement | void; const onErrorOverride : ?OnError = overrides.onError; let getProxyContainerOverride : ?GetProxyContainer = overrides.getProxyContainer; @@ -334,22 +336,6 @@ export function parentComponent({ uid, options, overrides = getDefaultO }); }; - const getProxyContainer : GetProxyContainer = (container : string | HTMLElement) : ZalgoPromise> => { - if (getProxyContainerOverride) { - return getProxyContainerOverride(container); - } - - return ZalgoPromise.try(() => { - return elementReady(container); - }).then(containerElement => { - if (isShadowElement(containerElement)) { - containerElement = insertShadowSlot(containerElement); - } - - return getProxyObject(containerElement); - }); - }; - const setProxyWin = (proxyWin : ProxyWindow) : ZalgoPromise => { if (setProxyWinOverride) { return setProxyWinOverride(proxyWin); @@ -926,17 +912,26 @@ export function parentComponent({ uid, options, overrides = getDefaultO const getProps = () => props; - const setProps = (newProps : PropsInputType

, isUpdate? : boolean = false) => { + const getDefaultPropsInput = () : PropsInputType

=> { + // $FlowFixMe + return {}; + }; + + const setProps = (newInputProps : PropsInputType

= getDefaultPropsInput()) => { if (__DEBUG__ && validate) { - validate({ props: newProps }); + validate({ props: newInputProps }); } + const container = currentContainer; const helpers = getHelpers(); - extendProps(propsDef, props, newProps, helpers, isUpdate); + extend(inputProps, newInputProps); + + // $FlowFixMe + extendProps(propsDef, props, inputProps, helpers, container); }; const updateProps = (newProps : PropsInputType

) : ZalgoPromise => { - setProps(newProps, true); + setProps(newProps); return initPromise.then(() => { const child = childComponent; @@ -959,6 +954,23 @@ export function parentComponent({ uid, options, overrides = getDefaultO }); }; + const getProxyContainer : GetProxyContainer = (container : ContainerReferenceType) : ZalgoPromise> => { + if (getProxyContainerOverride) { + return getProxyContainerOverride(container); + } + + return ZalgoPromise.try(() => { + return elementReady(container); + }).then(containerElement => { + if (isShadowElement(containerElement)) { + containerElement = insertShadowSlot(containerElement); + } + + currentContainer = containerElement; + return getProxyObject(containerElement); + }); + }; + const delegate = (context : $Values, target : CrossDomainWindowType) : ZalgoPromise => { const delegateProps = {}; for (const propName of Object.keys(props)) { @@ -1017,7 +1029,7 @@ export function parentComponent({ uid, options, overrides = getDefaultO }); }; - const checkAllowRender = (target : CrossDomainWindowType, childDomainMatch : DomainMatcher, container : string | HTMLElement) => { + const checkAllowRender = (target : CrossDomainWindowType, childDomainMatch : DomainMatcher, container : ContainerReferenceType) => { if (target === window) { return; } @@ -1058,13 +1070,20 @@ export function parentComponent({ uid, options, overrides = getDefaultO const watchForUnloadPromise = watchForUnload(); - const buildUrlPromise = buildUrl(); const buildBodyPromise = buildBody(); const onRenderPromise = event.trigger(EVENT.RENDER); const getProxyContainerPromise = getProxyContainer(container); const getProxyWindowPromise = getProxyWindow(); + const finalSetPropsPromise = getProxyContainerPromise.then(() => { + return setProps(); + }); + + const buildUrlPromise = finalSetPropsPromise.then(() => { + return buildUrl(); + }); + const buildWindowNamePromise = getProxyWindowPromise.then(proxyWin => { return buildWindowName({ proxyWin, initialChildDomain, childDomainMatch, target, context }); }); @@ -1143,7 +1162,7 @@ export function parentComponent({ uid, options, overrides = getDefaultO return ZalgoPromise.hash({ initPromise, buildUrlPromise, onRenderPromise, getProxyContainerPromise, openFramePromise, openPrerenderFramePromise, renderContainerPromise, openPromise, openPrerenderPromise, setStatePromise, prerenderPromise, loadUrlPromise, buildWindowNamePromise, setWindowNamePromise, watchForClosePromise, onDisplayPromise, - openBridgePromise, runTimeoutPromise, onRenderedPromise, delegatePromise, watchForUnloadPromise + openBridgePromise, runTimeoutPromise, onRenderedPromise, delegatePromise, watchForUnloadPromise, finalSetPropsPromise }); }).catch(err => { diff --git a/src/parent/props.js b/src/parent/props.js index e7200d95..0fe3ca93 100644 --- a/src/parent/props.js +++ b/src/parent/props.js @@ -1,103 +1,80 @@ /* @flow */ import { ZalgoPromise } from 'zalgo-promise/src'; -import { dotify, isDefined, extend, base64encode } from 'belter/src'; +import { dotify, isDefined, base64encode, noop } from 'belter/src'; import { eachProp, mapProps, type PropsInputType, type PropsType, type PropsDefinitionType } from '../component/props'; -import { PROP_SERIALIZATION, METHOD } from '../constants'; +import { PROP_SERIALIZATION, METHOD, PROP_TYPE } from '../constants'; import type { ParentHelpers } from './index'; -function getDefaultInputProps

() : P { - // $FlowFixMe[incompatible-type] - const defaultInputProps : P = {}; - return defaultInputProps; -} - -export function extendProps(propsDef : PropsDefinitionType, props : PropsType

, inputProps : PropsInputType

, helpers : ParentHelpers

, isUpdate : boolean = false) { - - inputProps = inputProps || getDefaultInputProps(); - extend(props, inputProps); - - const propNames = isUpdate ? [] : [ ...Object.keys(propsDef) ]; - - for (const key of Object.keys(inputProps)) { - if (propNames.indexOf(key) === -1) { - propNames.push(key); - } - } - - const aliases = []; - +export function extendProps(propsDef : PropsDefinitionType, existingProps : PropsType

, inputProps : PropsInputType

, helpers : ParentHelpers

, container : HTMLElement | void) { const { state, close, focus, event, onError } = helpers; - for (const key of propNames) { - const propDef = propsDef[key]; - - // $FlowFixMe - let value = inputProps[key]; + // $FlowFixMe + eachProp(inputProps, propsDef, (key, propDef, val) => { + let valueDetermined = false; + let value = val; - if (!propDef) { - continue; - } + const getDerivedValue = () => { + if (!propDef) { + return value; + } - const alias = propDef.alias; - if (alias) { - if (!isDefined(value) && isDefined(inputProps[alias])) { + const alias = propDef.alias; + if (alias && !isDefined(val) && isDefined(inputProps[alias])) { value = inputProps[alias]; } - aliases.push(alias); - } + + if (propDef.value) { + value = propDef.value({ props: existingProps, state, close, focus, event, onError, container }); + } + + if (propDef.default && !isDefined(value) && !isDefined(inputProps[key])) { + value = propDef.default({ props: existingProps, state, close, focus, event, onError, container }); + } - if (propDef.value) { - value = propDef.value({ props, state, close, focus, event, onError }); - } + if (isDefined(value)) { + if (propDef.type === PROP_TYPE.ARRAY ? !Array.isArray(value) : (typeof value !== propDef.type)) { + throw new TypeError(`Prop is not of type ${ propDef.type }: ${ key }`); + } + } else { + if (propDef.required !== false && !isDefined(inputProps[key])) { + throw new Error(`Expected prop "${ key }" to be defined`); + } + } + + if (__DEBUG__ && isDefined(value) && propDef.validate) { + // $FlowFixMe + propDef.validate({ value, props: inputProps }); + } - if (!isDefined(value) && propDef.default) { - value = propDef.default({ props, state, close, focus, event, onError }); - } + if (isDefined(value) && propDef.decorate) { + // $FlowFixMe + value = propDef.decorate({ value, props: existingProps, state, close, focus, event, onError, container }); + } + + return value; + }; - if (isDefined(value)) { - if (propDef.type === 'array' ? !Array.isArray(value) : (typeof value !== propDef.type)) { - throw new TypeError(`Prop is not of type ${ propDef.type }: ${ key }`); + const getter = () => { + if (valueDetermined) { + return value; } - } - - // $FlowFixMe - props[key] = value; - } - - for (const alias of aliases) { - delete props[alias]; - } - - eachProp(props, propsDef, (key, propDef, value) => { - if (!propDef) { - return; - } - - if (__DEBUG__ && isDefined(value) && propDef.validate) { - // $FlowFixMe[incompatible-call] - // $FlowFixMe[incompatible-exact] - propDef.validate({ value, props }); - } - - if (isDefined(value) && propDef.decorate) { - // $FlowFixMe[incompatible-call] - const decoratedValue = propDef.decorate({ value, props, state, close, focus, event, onError }); - // $FlowFixMe[incompatible-type] - props[key] = decoratedValue; - } + + valueDetermined = true; + return getDerivedValue(); + }; + + Object.defineProperty(existingProps, key, { + configurable: true, + enumerable: true, + get: getter + }); }); - for (const key of Object.keys(propsDef)) { - const propDef = propsDef[key]; - // $FlowFixMe - const propVal = props[key]; - if (propDef.required !== false && !isDefined(propVal)) { - throw new Error(`Expected prop "${ key }" to be defined`); - } - } + // $FlowFixMe + eachProp(existingProps, propsDef, noop); } export function serializeProps(propsDef : PropsDefinitionType, props : (PropsType

), method : $Values) : ZalgoPromise<{ [string] : string | boolean }> { @@ -107,7 +84,7 @@ export function serializeProps(propsDef : PropsDefinitionType, props return ZalgoPromise.all(mapProps(props, propsDef, (key, propDef, value) => { return ZalgoPromise.resolve().then(() => { - if (value === null || typeof value === 'undefined') { + if (value === null || typeof value === 'undefined' || !propDef) { return; } diff --git a/src/types.js b/src/types.js index be621a93..1a123643 100644 --- a/src/types.js +++ b/src/types.js @@ -18,3 +18,5 @@ export type CancelableType = {| |}; export type StringMatcherType = string | $ReadOnlyArray | RegExp; + +export type ContainerReferenceType = string | HTMLElement; diff --git a/test/tests/props.js b/test/tests/props.js index 957c5ed6..0f36a09b 100644 --- a/test/tests/props.js +++ b/test/tests/props.js @@ -1,7 +1,7 @@ /* @flow */ /* eslint max-lines: off */ -import { wrapPromise, noop, parseQuery, destroyElement } from 'belter/src'; +import { wrapPromise, noop, parseQuery, destroyElement, getElement } from 'belter/src'; import { ZalgoPromise } from 'zalgo-promise/src'; import { onWindowOpen, getBody } from '../common'; @@ -46,6 +46,141 @@ describe('zoid props cases', () => { }); }); + it('should render a component with a prop with a pre-defined value using container', () => { + return wrapPromise(({ expect }) => { + const bodyWidth = getBody().offsetWidth; + + window.__component__ = () => { + return zoid.create({ + tag: 'test-prop-value-container', + url: 'mock://www.child.com/base/test/windows/child/index.htm', + domain: 'mock://www.child.com', + props: { + foo: { + type: 'number', + required: false, + value: ({ container }) => { + return container?.offsetWidth; + } + }, + passFoo: { + type: 'function', + required: true + } + } + }); + }; + + const component = window.__component__(); + const instance = component({ + run: () => ` + window.xprops.passFoo({ foo: xprops.foo }); + `, + passFoo: expect('passFoo', ({ foo }) => { + if (foo !== bodyWidth) { + throw new Error(`Expected prop to have the correct value; got ${ foo }`); + } + }) + }); + + return instance.render(getBody()); + }); + }); + + it('should render a component with a prop with a pre-defined value using container, and pass the value in the url', () => { + return wrapPromise(({ expect }) => { + const bodyWidth = getBody().offsetWidth; + + window.__component__ = () => { + return zoid.create({ + tag: 'test-prop-value-container-url-param', + url: 'mock://www.child.com/base/test/windows/child/index.htm', + domain: 'mock://www.child.com', + props: { + foo: { + type: 'number', + required: false, + value: ({ container }) => { + return container ? getElement(container)?.offsetWidth : null; + }, + queryParam: true + }, + passFoo: { + type: 'function', + required: true + } + } + }); + }; + + const component = window.__component__(); + const instance = component({ + run: () => ` + window.xprops.passFoo({ query: location.search }); + `, + passFoo: expect('passFoo', ({ query }) => { + if (query.indexOf(`foo=${ bodyWidth }`) === -1) { + throw new Error(`Expected foo=${ bodyWidth } in the url`); + } + }) + }); + + return instance.render(getBody()); + }); + }); + + it('should render a component with a prop with a pre-defined value using container, and pass the value in the url, with a lazy element', () => { + return wrapPromise(({ expect }) => { + const bodyWidth = getBody().offsetWidth; + + window.__component__ = () => { + return zoid.create({ + tag: 'test-prop-value-container-url-param-element-not-ready', + url: 'mock://www.child.com/base/test/windows/child/index.htm', + domain: 'mock://www.child.com', + props: { + foo: { + type: 'number', + required: false, + value: ({ container }) => { + return container ? getElement(container)?.offsetWidth : null; + }, + queryParam: true + }, + passFoo: { + type: 'function', + required: true + } + } + }); + }; + + const component = window.__component__(); + const instance = component({ + run: () => ` + window.xprops.passFoo({ query: location.search }); + `, + passFoo: expect('passFoo', ({ query }) => { + if (query.indexOf(`foo=${ bodyWidth }`) === -1) { + throw new Error(`Expected foo=${ bodyWidth } in the url`); + } + }) + }); + + Object.defineProperty(document, 'readyState', { value: 'loading', configurable: true }); + + const renderPromise = instance.render('#container-element'); + + const container = document.createElement('div'); + container.setAttribute('id', 'container-element'); + getBody().appendChild(container); + + Object.defineProperty(document, 'readyState', { value: 'ready', configurable: true }); + + return renderPromise; + }); + }); + it('should enter a component, update a prop, and call a prop', () => { return wrapPromise(({ expect }) => { @@ -2075,7 +2210,6 @@ describe('zoid props cases', () => { } }); }; - const component = window.__component__(); const instance = component({ @@ -2087,8 +2221,6 @@ describe('zoid props cases', () => { if (props.bar !== barValue) { throw new Error(`Expected props.bar to have the correct value of ${ barValue }; got ${ props.bar }`); } - - /* if (props.superprop.foo !== fooValue) { throw new Error(`Expected props.superprop.foo to have the correct value of ${ fooValue }; got ${ props.superprop.foo }`); @@ -2098,8 +2230,6 @@ describe('zoid props cases', () => { throw new Error(`Expected props.superprop.bar to have the correct value of ${ barValue }; got ${ props.superprop.bar }`); } - */ - if (typeof props.superpropfunc !== 'function') { throw new TypeError(`Expected props.superpropfunc to have the correct type of 'function'; got ${ typeof props.superpropfunc }`); }