From 7f0a93e73292f236f94646b5c2eb309c07fee45d Mon Sep 17 00:00:00 2001 From: Paul Doyle Date: Thu, 9 Jan 2020 15:10:48 -0800 Subject: [PATCH 01/10] Rename old context --- src/Component.lua | 4 ++-- src/createReconciler.lua | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Component.lua b/src/Component.lua index 5782af29..35b9ed9c 100644 --- a/src/Component.lua +++ b/src/Component.lua @@ -273,7 +273,7 @@ function Component:__mount(reconciler, virtualNode) instance.props = props - local newContext = assign({}, virtualNode.context) + local newContext = assign({}, virtualNode.legacyContext) instance._context = newContext instance.state = assign({}, instance:__getDerivedState(instance.props, {})) @@ -284,7 +284,7 @@ function Component:__mount(reconciler, virtualNode) end -- It's possible for init() to redefine _context! - virtualNode.context = instance._context + virtualNode.legacyContext = instance._context internalData.lifecyclePhase = ComponentLifecyclePhase.Render local renderResult = instance:render() diff --git a/src/createReconciler.lua b/src/createReconciler.lua index fbae970d..b529a845 100644 --- a/src/createReconciler.lua +++ b/src/createReconciler.lua @@ -31,16 +31,16 @@ local function createReconciler(renderer) Unmount the given virtualNode, replacing it with a new node described by the given element. - Preserves host properties, depth, and context from parent. + Preserves host properties, depth, and legacyContext from parent. ]] local function replaceVirtualNode(virtualNode, newElement) local hostParent = virtualNode.hostParent local hostKey = virtualNode.hostKey local depth = virtualNode.depth - local parentContext = virtualNode.parentContext + local parentLegacyContext = virtualNode.parentLegacyContext unmountVirtualNode(virtualNode) - local newNode = mountVirtualNode(newElement, hostParent, hostKey, parentContext) + local newNode = mountVirtualNode(newElement, hostParent, hostKey, parentLegacyContext) -- mountVirtualNode can return nil if the element is a boolean if newNode ~= nil then @@ -85,7 +85,7 @@ local function createReconciler(renderer) end if virtualNode.children[childKey] == nil then - local childNode = mountVirtualNode(newElement, hostParent, concreteKey, virtualNode.context) + local childNode = mountVirtualNode(newElement, hostParent, concreteKey, virtualNode.legacyContext) -- mountVirtualNode can return nil if the element is a boolean if childNode ~= nil then @@ -247,10 +247,10 @@ local function createReconciler(renderer) --[[ Constructs a new virtual node but not does mount it. ]] - local function createVirtualNode(element, hostParent, hostKey, context) + local function createVirtualNode(element, hostParent, hostKey, legacyContext) if config.internalTypeChecks then internalAssert(renderer.isHostObject(hostParent) or hostParent == nil, "Expected arg #2 to be a host object") - internalAssert(typeof(context) == "table" or context == nil, "Expected arg #4 to be of type table or nil") + internalAssert(typeof(legacyContext) == "table" or legacyContext == nil, "Expected arg #4 to be of type table or nil") end if config.typeChecks then assert(hostKey ~= nil, "Expected arg #3 to be non-nil") @@ -267,10 +267,10 @@ local function createReconciler(renderer) children = {}, hostParent = hostParent, hostKey = hostKey, - context = context, - -- This copy of context is useful if the element gets replaced + legacyContext = legacyContext, + -- This copy of legacyContext is useful if the element gets replaced -- with an element of a different component type - parentContext = context, + parentLegacyContext = legacyContext, } end @@ -304,10 +304,10 @@ local function createReconciler(renderer) Constructs a new virtual node and mounts it, but does not place it into the tree. ]] - function mountVirtualNode(element, hostParent, hostKey, context) + function mountVirtualNode(element, hostParent, hostKey, legacyContext) if config.internalTypeChecks then internalAssert(renderer.isHostObject(hostParent) or hostParent == nil, "Expected arg #2 to be a host object") - internalAssert(typeof(context) == "table" or context == nil, "Expected arg #4 to be of type table or nil") + internalAssert(typeof(legacyContext) == "table" or legacyContext == nil, "Expected arg #4 to be of type table or nil") end if config.typeChecks then assert(hostKey ~= nil, "Expected arg #3 to be non-nil") @@ -324,7 +324,7 @@ local function createReconciler(renderer) local kind = ElementKind.of(element) - local virtualNode = createVirtualNode(element, hostParent, hostKey, context) + local virtualNode = createVirtualNode(element, hostParent, hostKey, legacyContext) if kind == ElementKind.Host then renderer.mountHostNode(reconciler, virtualNode) From 4a73ded8d624e064a48ed3a023933efb558414dc Mon Sep 17 00:00:00 2001 From: Paul Doyle Date: Thu, 9 Jan 2020 15:25:26 -0800 Subject: [PATCH 02/10] Rename context to legacyContext in tests --- .../{context.spec.lua => legacyContext.spec.lua} | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) rename src/Component.spec/{context.spec.lua => legacyContext.spec.lua} (91%) diff --git a/src/Component.spec/context.spec.lua b/src/Component.spec/legacyContext.spec.lua similarity index 91% rename from src/Component.spec/context.spec.lua rename to src/Component.spec/legacyContext.spec.lua index cfef942d..bee7462d 100644 --- a/src/Component.spec/context.spec.lua +++ b/src/Component.spec/legacyContext.spec.lua @@ -27,7 +27,7 @@ return function() foo = "bar", } - assertDeepEqual(node.context, expectedContext) + assertDeepEqual(node.legacyContext, expectedContext) end) it("should be inherited from parent stateful nodes", function() @@ -57,8 +57,8 @@ return function() local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, context) expect(capturedContext).never.to.equal(context) - expect(capturedContext).never.to.equal(node.context) - assertDeepEqual(node.context, context) + expect(capturedContext).never.to.equal(node.legacyContext) + assertDeepEqual(node.legacyContext, context) assertDeepEqual(capturedContext, context) end) @@ -87,8 +87,8 @@ return function() local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, context) expect(capturedContext).never.to.equal(context) - expect(capturedContext).never.to.equal(node.context) - assertDeepEqual(node.context, context) + expect(capturedContext).never.to.equal(node.legacyContext) + assertDeepEqual(node.legacyContext, context) assertDeepEqual(capturedContext, context) end) @@ -131,12 +131,12 @@ return function() } -- Because components mutate context, we're careful with equality - expect(node.context).never.to.equal(context) + expect(node.legacyContext).never.to.equal(context) expect(capturedContext).never.to.equal(context) - expect(capturedContext).never.to.equal(node.context) + expect(capturedContext).never.to.equal(node.legacyContext) assertDeepEqual(context, initialContext) - assertDeepEqual(node.context, expectedContext) + assertDeepEqual(node.legacyContext, expectedContext) assertDeepEqual(capturedContext, expectedContext) end) From 12515d2847c740adf9ba6ffae01801caed5c9ed9 Mon Sep 17 00:00:00 2001 From: Paul Doyle Date: Thu, 9 Jan 2020 16:48:54 -0800 Subject: [PATCH 03/10] Add context api to component --- src/Component.lua | 29 +++++++++++++++++++++++++++++ src/createReconciler.lua | 22 +++++++++++++++++----- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/Component.lua b/src/Component.lua index 35b9ed9c..416f2c44 100644 --- a/src/Component.lua +++ b/src/Component.lua @@ -196,6 +196,35 @@ function Component:render() error(message, 0) end +--[[ + Retrieves the context object corresponding to the given key +]] +function Component:__getContext(key) + if config.internalTypeChecks then + internalAssert(Type.of(self) == Type.StatefulComponentInstance, "Invalid use of `__getContext`") + end + + local virtualNode = self[InternalData].virtualNode + local context = virtualNode.context or virtualNode.inheritedContext + + return context[key] +end + +--[[ + Adds new context property to this component's context map (which will be + passed down to child components) +]] +function Component:__addContext(key, value) + if config.internalTypeChecks then + internalAssert(Type.of(self) == Type.StatefulComponentInstance, "Invalid use of `__addContext`") + end + + local existing = self:__getContext() + local virtualNode = self[InternalData].virtualNode + + virtualNode.context = assign({}, existing, { key = value }) +end + --[[ Performs property validation if the static method validateProps is declared. validateProps should follow assert's expected arguments: diff --git a/src/createReconciler.lua b/src/createReconciler.lua index b529a845..ad51e4bf 100644 --- a/src/createReconciler.lua +++ b/src/createReconciler.lua @@ -38,9 +38,10 @@ local function createReconciler(renderer) local hostKey = virtualNode.hostKey local depth = virtualNode.depth local parentLegacyContext = virtualNode.parentLegacyContext + local inheritedContext = virtualNode.inheritedContext unmountVirtualNode(virtualNode) - local newNode = mountVirtualNode(newElement, hostParent, hostKey, parentLegacyContext) + local newNode = mountVirtualNode(newElement, hostParent, hostKey, inheritedContext, parentLegacyContext) -- mountVirtualNode can return nil if the element is a boolean if newNode ~= nil then @@ -85,7 +86,13 @@ local function createReconciler(renderer) end if virtualNode.children[childKey] == nil then - local childNode = mountVirtualNode(newElement, hostParent, concreteKey, virtualNode.legacyContext) + local childNode = mountVirtualNode( + newElement, + hostParent, + concreteKey, + virtualNode.context, + virtualNode.legacyContext + ) -- mountVirtualNode can return nil if the element is a boolean if childNode ~= nil then @@ -247,7 +254,7 @@ local function createReconciler(renderer) --[[ Constructs a new virtual node but not does mount it. ]] - local function createVirtualNode(element, hostParent, hostKey, legacyContext) + local function createVirtualNode(element, hostParent, hostKey, context, legacyContext) if config.internalTypeChecks then internalAssert(renderer.isHostObject(hostParent) or hostParent == nil, "Expected arg #2 to be a host object") internalAssert(typeof(legacyContext) == "table" or legacyContext == nil, "Expected arg #4 to be of type table or nil") @@ -271,6 +278,11 @@ local function createReconciler(renderer) -- This copy of legacyContext is useful if the element gets replaced -- with an element of a different component type parentLegacyContext = legacyContext, + + -- New context api + inheritedContext = context, + -- will only be populated if the component modifies context + context = nil, } end @@ -304,7 +316,7 @@ local function createReconciler(renderer) Constructs a new virtual node and mounts it, but does not place it into the tree. ]] - function mountVirtualNode(element, hostParent, hostKey, legacyContext) + function mountVirtualNode(element, hostParent, hostKey, context, legacyContext) if config.internalTypeChecks then internalAssert(renderer.isHostObject(hostParent) or hostParent == nil, "Expected arg #2 to be a host object") internalAssert(typeof(legacyContext) == "table" or legacyContext == nil, "Expected arg #4 to be of type table or nil") @@ -324,7 +336,7 @@ local function createReconciler(renderer) local kind = ElementKind.of(element) - local virtualNode = createVirtualNode(element, hostParent, hostKey, legacyContext) + local virtualNode = createVirtualNode(element, hostParent, hostKey, context, legacyContext) if kind == ElementKind.Host then renderer.mountHostNode(reconciler, virtualNode) From 4b830a47a2cee08d86af558a4f369a8d3cf2139b Mon Sep 17 00:00:00 2001 From: Paul Doyle Date: Thu, 9 Jan 2020 17:16:08 -0800 Subject: [PATCH 04/10] Rearrange formatting, flesh out comments --- src/Component.lua | 14 +++++++++++--- src/createReconciler.lua | 32 ++++++++++++++++++++++++-------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/Component.lua b/src/Component.lua index 416f2c44..0aadc8de 100644 --- a/src/Component.lua +++ b/src/Component.lua @@ -205,7 +205,7 @@ function Component:__getContext(key) end local virtualNode = self[InternalData].virtualNode - local context = virtualNode.context or virtualNode.inheritedContext + local context = virtualNode.context return context[key] end @@ -218,10 +218,18 @@ function Component:__addContext(key, value) if config.internalTypeChecks then internalAssert(Type.of(self) == Type.StatefulComponentInstance, "Invalid use of `__addContext`") end - - local existing = self:__getContext() local virtualNode = self[InternalData].virtualNode + -- If we don't already have the component's original, unmodified context + -- stored in the virtual node, store it now so we can restore it if the node + -- gets replaced by a different component + if virtualNode.originalContext == nil then + virtualNode.originalContext = virtualNode.context + end + + -- Build a new context table, on top of the existing one, and apply it to + -- our node + local existing = self:__getContext() virtualNode.context = assign({}, existing, { key = value }) end diff --git a/src/createReconciler.lua b/src/createReconciler.lua index ad51e4bf..0202f3f0 100644 --- a/src/createReconciler.lua +++ b/src/createReconciler.lua @@ -37,11 +37,15 @@ local function createReconciler(renderer) local hostParent = virtualNode.hostParent local hostKey = virtualNode.hostKey local depth = virtualNode.depth + + -- If the node that is being replaced has modified context, we need to + -- use the original *unmodified* context for the new node + -- The `originalContext` field will be nil if the context was unchanged + local context = virtualNode.originalContext or virtualNode.context local parentLegacyContext = virtualNode.parentLegacyContext - local inheritedContext = virtualNode.inheritedContext unmountVirtualNode(virtualNode) - local newNode = mountVirtualNode(newElement, hostParent, hostKey, inheritedContext, parentLegacyContext) + local newNode = mountVirtualNode(newElement, hostParent, hostKey, context, parentLegacyContext) -- mountVirtualNode can return nil if the element is a boolean if newNode ~= nil then @@ -257,7 +261,10 @@ local function createReconciler(renderer) local function createVirtualNode(element, hostParent, hostKey, context, legacyContext) if config.internalTypeChecks then internalAssert(renderer.isHostObject(hostParent) or hostParent == nil, "Expected arg #2 to be a host object") - internalAssert(typeof(legacyContext) == "table" or legacyContext == nil, "Expected arg #4 to be of type table or nil") + internalAssert( + typeof(legacyContext) == "table" or legacyContext == nil, + "Expected arg #4 to be of type table or nil" + ) end if config.typeChecks then assert(hostKey ~= nil, "Expected arg #3 to be non-nil") @@ -274,15 +281,21 @@ local function createReconciler(renderer) children = {}, hostParent = hostParent, hostKey = hostKey, + + -- Legacy Context API legacyContext = legacyContext, -- This copy of legacyContext is useful if the element gets replaced -- with an element of a different component type parentLegacyContext = legacyContext, - -- New context api - inheritedContext = context, - -- will only be populated if the component modifies context - context = nil, + -- Context API + -- The inherited context from this node's parent + context = context, + + -- A saved copy of the unmodified context; this will be saved when + -- a component adds new context, and will be used when a component + -- is replaced + originalContext = nil, } end @@ -319,7 +332,10 @@ local function createReconciler(renderer) function mountVirtualNode(element, hostParent, hostKey, context, legacyContext) if config.internalTypeChecks then internalAssert(renderer.isHostObject(hostParent) or hostParent == nil, "Expected arg #2 to be a host object") - internalAssert(typeof(legacyContext) == "table" or legacyContext == nil, "Expected arg #4 to be of type table or nil") + internalAssert( + typeof(legacyContext) == "table" or legacyContext == nil, + "Expected arg #4 to be of type table or nil" + ) end if config.typeChecks then assert(hostKey ~= nil, "Expected arg #3 to be non-nil") From 8e6fa2621f584e04ac10fd9dd2b1b4e79594f37c Mon Sep 17 00:00:00 2001 From: Paul Doyle Date: Fri, 10 Jan 2020 09:50:08 -0800 Subject: [PATCH 05/10] Fix some minor bugs --- src/Component.lua | 7 ++++--- src/createReconciler.lua | 15 ++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Component.lua b/src/Component.lua index 0aadc8de..aed48ce3 100644 --- a/src/Component.lua +++ b/src/Component.lua @@ -202,6 +202,7 @@ end function Component:__getContext(key) if config.internalTypeChecks then internalAssert(Type.of(self) == Type.StatefulComponentInstance, "Invalid use of `__getContext`") + internalAssert(key ~= nil, "Context key cannot be nil") end local virtualNode = self[InternalData].virtualNode @@ -211,7 +212,7 @@ function Component:__getContext(key) end --[[ - Adds new context property to this component's context map (which will be + Adds new context property to this component's context table (which will be passed down to child components) ]] function Component:__addContext(key, value) @@ -229,8 +230,8 @@ function Component:__addContext(key, value) -- Build a new context table, on top of the existing one, and apply it to -- our node - local existing = self:__getContext() - virtualNode.context = assign({}, existing, { key = value }) + local existing = virtualNode.context + virtualNode.context = assign({}, existing, { [key] = value }) end --[[ diff --git a/src/createReconciler.lua b/src/createReconciler.lua index 0202f3f0..a0b29d7b 100644 --- a/src/createReconciler.lua +++ b/src/createReconciler.lua @@ -261,6 +261,7 @@ local function createReconciler(renderer) local function createVirtualNode(element, hostParent, hostKey, context, legacyContext) if config.internalTypeChecks then internalAssert(renderer.isHostObject(hostParent) or hostParent == nil, "Expected arg #2 to be a host object") + internalAssert(typeof(context) == "table" or context == nil, "Expected arg #4 to be of type table or nil") internalAssert( typeof(legacyContext) == "table" or legacyContext == nil, "Expected arg #4 to be of type table or nil" @@ -283,18 +284,18 @@ local function createReconciler(renderer) hostKey = hostKey, -- Legacy Context API + -- A table of context values inherited from the parent node legacyContext = legacyContext, - -- This copy of legacyContext is useful if the element gets replaced - -- with an element of a different component type + + -- A saved copy of the parent context, used when replacing a node parentLegacyContext = legacyContext, -- Context API - -- The inherited context from this node's parent - context = context, + -- A table of context values inherited from the parent node + context = context or {}, - -- A saved copy of the unmodified context; this will be saved when - -- a component adds new context, and will be used when a component - -- is replaced + -- A saved copy of the unmodified context; this will be updated when + -- a component adds new context and used when a node is replaced originalContext = nil, } end From aeee1dc0eed15104fff154efb5fbc759b6dec4de Mon Sep 17 00:00:00 2001 From: Paul Doyle Date: Fri, 10 Jan 2020 09:50:29 -0800 Subject: [PATCH 06/10] Fix up old tests --- src/Component.spec/legacyContext.spec.lua | 6 +++--- src/RobloxRenderer.spec.lua | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Component.spec/legacyContext.spec.lua b/src/Component.spec/legacyContext.spec.lua index bee7462d..e1014f21 100644 --- a/src/Component.spec/legacyContext.spec.lua +++ b/src/Component.spec/legacyContext.spec.lua @@ -54,7 +54,7 @@ return function() hello = "world", value = 6, } - local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, context) + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, nil, context) expect(capturedContext).never.to.equal(context) expect(capturedContext).never.to.equal(node.legacyContext) @@ -84,7 +84,7 @@ return function() hello = "world", value = 6, } - local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, context) + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, nil, context) expect(capturedContext).never.to.equal(context) expect(capturedContext).never.to.equal(node.legacyContext) @@ -119,7 +119,7 @@ return function() local context = { dont = "try it", } - local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, context) + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, nil, context) local initialContext = { dont = "try it", diff --git a/src/RobloxRenderer.spec.lua b/src/RobloxRenderer.spec.lua index d31421c9..f1cc2085 100644 --- a/src/RobloxRenderer.spec.lua +++ b/src/RobloxRenderer.spec.lua @@ -805,7 +805,7 @@ return function() end) end) - describe("Context", function() + describe("Legacy context", function() it("should pass context values through Roblox host nodes", function() local Consumer = Component:extend("Consumer") @@ -825,7 +825,7 @@ return function() local context = { hello = "world", } - local node = reconciler.mountVirtualNode(element, hostParent, hostKey, context) + local node = reconciler.mountVirtualNode(element, hostParent, hostKey, nil, context) expect(capturedContext).never.to.equal(context) assertDeepEqual(capturedContext, context) From ed497d8dfece1160b01d862dca671fa3c5f7a510 Mon Sep 17 00:00:00 2001 From: Paul Doyle Date: Fri, 10 Jan 2020 11:29:51 -0800 Subject: [PATCH 07/10] Get Component test parity for new context, add a couple other tests --- src/Component.spec/context.spec.lua | 297 ++++++++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 src/Component.spec/context.spec.lua diff --git a/src/Component.spec/context.spec.lua b/src/Component.spec/context.spec.lua new file mode 100644 index 00000000..1fe34d4b --- /dev/null +++ b/src/Component.spec/context.spec.lua @@ -0,0 +1,297 @@ +return function() + local assertDeepEqual = require(script.Parent.Parent.assertDeepEqual) + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + local oneChild = require(script.Parent.Parent.oneChild) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should be provided as an internal api on Component", function() + local Provider = Component:extend("Provider") + + function Provider:init() + self:__addContext("foo", "bar") + end + + function Provider:render() + end + + local element = createElement(Provider) + local hostParent = nil + local hostKey = "Provider" + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + local expectedContext = { + foo = "bar", + } + + assertDeepEqual(node.context, expectedContext) + end) + + it("should be inherited from parent stateful nodes", function() + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = { + hello = self:__getContext("hello"), + value = self:__getContext("value"), + } + end + + function Consumer:render() + end + + local Parent = Component:extend("Parent") + + function Parent:render() + return createElement(Consumer) + end + + local element = createElement(Parent) + local hostParent = nil + local hostKey = "Parent" + local context = { + hello = "world", + value = 6, + } + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, context) + + expect(capturedContext).never.to.equal(context) + expect(capturedContext).never.to.equal(node.context) + assertDeepEqual(node.context, context) + assertDeepEqual(capturedContext, context) + end) + + it("should be inherited from parent function nodes", function() + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = { + hello = self:__getContext("hello"), + value = self:__getContext("value"), + } + end + + function Consumer:render() + end + + local function Parent() + return createElement(Consumer) + end + + local element = createElement(Parent) + local hostParent = nil + local hostKey = "Parent" + local context = { + hello = "world", + value = 6, + } + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, context) + + expect(capturedContext).never.to.equal(context) + expect(capturedContext).never.to.equal(node.context) + assertDeepEqual(node.context, context) + assertDeepEqual(capturedContext, context) + end) + + it("should not copy the context table if it doesn't need to", function() + local Parent = Component:extend("Parent") + + function Parent:init() + self:__addContext("parent", "I'm here!") + end + + function Parent:render() + -- Create some child element + return createElement(function() end) + end + + local element = createElement(Parent) + local hostParent = nil + local hostKey = "Parent" + local parentNode = noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + local expectedContext = { + parent = "I'm here!", + } + + assertDeepEqual(parentNode.context, expectedContext) + + local childNode = oneChild(parentNode.children) + + -- Parent and child should have the same context table + expect(parentNode.context, childNode.context) + end) + + it("should not allow context to move up the tree", function() + local ChildProvider = Component:extend("ChildProvider") + + function ChildProvider:init() + self:__addContext("child", "I'm here too!") + end + + function ChildProvider:render() + end + + local ParentProvider = Component:extend("ParentProvider") + + function ParentProvider:init() + self:__addContext("parent", "I'm here!") + end + + function ParentProvider:render() + return createElement(ChildProvider) + end + + local element = createElement(ParentProvider) + local hostParent = nil + local hostKey = "Parent" + + local parentNode = noopReconciler.mountVirtualNode(element, hostParent, hostKey) + local childNode = oneChild(parentNode.children) + + local expectedParentContext = { + parent = "I'm here!", + -- Context does not travel back up + } + + local expectedChildContext = { + parent = "I'm here!", + child = "I'm here too!" + } + + assertDeepEqual(parentNode.context, expectedParentContext) + assertDeepEqual(childNode.context, expectedChildContext) + end) + + it("should contain values put into the tree by parent nodes", function() + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = { + dont = self:__getContext("dont"), + frob = self:__getContext("frob"), + } + end + + function Consumer:render() + end + + local Provider = Component:extend("Provider") + + function Provider:init() + self:__addContext("frob", "ulator") + end + + function Provider:render() + return createElement(Consumer) + end + + local element = createElement(Provider) + local hostParent = nil + local hostKey = "Consumer" + local context = { + dont = "try it", + } + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, context) + + local initialContext = { + dont = "try it", + } + + local expectedContext = { + dont = "try it", + frob = "ulator", + } + + -- Because components mutate context, we're careful with equality + expect(node.context).never.to.equal(context) + expect(capturedContext).never.to.equal(context) + expect(capturedContext).never.to.equal(node.context) + + assertDeepEqual(context, initialContext) + assertDeepEqual(node.context, expectedContext) + assertDeepEqual(capturedContext, expectedContext) + end) + + it("should transfer context to children that are replaced", function() + local ConsumerA = Component:extend("ConsumerA") + + local function captureAllContext(component) + return { + A = component:__getContext("A"), + B = component:__getContext("B"), + frob = component:__getContext("frob"), + } + end + + local capturedContextA + function ConsumerA:init() + self:__addContext("A", "hello") + + capturedContextA = captureAllContext(self) + end + + function ConsumerA:render() + end + + local ConsumerB = Component:extend("ConsumerB") + + local capturedContextB + function ConsumerB:init() + self:__addContext("B", "hello") + + capturedContextB = captureAllContext(self) + end + + function ConsumerB:render() + end + + local Provider = Component:extend("Provider") + + function Provider:init() + self:__addContext("frob", "ulator") + end + + function Provider:render() + local useConsumerB = self.props.useConsumerB + + if useConsumerB then + return createElement(ConsumerB) + else + return createElement(ConsumerA) + end + end + + local hostParent = nil + local hostKey = "Consumer" + + local element = createElement(Provider) + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + local expectedContextA = { + frob = "ulator", + A = "hello", + } + + assertDeepEqual(capturedContextA, expectedContextA) + + local expectedContextB = { + frob = "ulator", + B = "hello", + } + + local replacedElement = createElement(Provider, { + useConsumerB = true, + }) + noopReconciler.updateVirtualNode(node, replacedElement) + + assertDeepEqual(capturedContextB, expectedContextB) + end) +end \ No newline at end of file From d5a2bd533e2e086937dcb518204dbfd7ff6b8cc4 Mon Sep 17 00:00:00 2001 From: Paul Doyle Date: Fri, 10 Jan 2020 11:32:09 -0800 Subject: [PATCH 08/10] Restore test parity for context tests in renderer --- src/RobloxRenderer.spec.lua | 74 +++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/RobloxRenderer.spec.lua b/src/RobloxRenderer.spec.lua index f1cc2085..9bbf2e7d 100644 --- a/src/RobloxRenderer.spec.lua +++ b/src/RobloxRenderer.spec.lua @@ -805,6 +805,79 @@ return function() end) end) + describe("Context", function() + it("should pass context values through Roblox host nodes", function() + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = { + hello = self:__getContext("hello") + } + end + + function Consumer:render() + end + + local element = createElement("Folder", nil, { + Consumer = createElement(Consumer) + }) + local hostParent = nil + local hostKey = "Context Test" + local context = { + hello = "world", + } + local node = reconciler.mountVirtualNode(element, hostParent, hostKey, context) + + expect(capturedContext).never.to.equal(context) + assertDeepEqual(capturedContext, context) + + reconciler.unmountVirtualNode(node) + end) + + it("should pass context values through portal nodes", function() + local target = Instance.new("Folder") + + local Provider = Component:extend("Provider") + + function Provider:init() + self:__addContext("foo", "bar") + end + + function Provider:render() + return createElement("Folder", nil, self.props[Children]) + end + + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = { + foo = self:__getContext("foo"), + } + end + + function Consumer:render() + return nil + end + + local element = createElement(Provider, nil, { + Portal = createElement(Portal, { + target = target, + }, { + Consumer = createElement(Consumer), + }) + }) + local hostParent = nil + local hostKey = "Some Key" + reconciler.mountVirtualNode(element, hostParent, hostKey) + + assertDeepEqual(capturedContext, { + foo = "bar" + }) + end) + end) + describe("Legacy context", function() it("should pass context values through Roblox host nodes", function() local Consumer = Component:extend("Consumer") @@ -873,4 +946,5 @@ return function() }) end) end) + end \ No newline at end of file From 9fb5093adbbefcaa55ab796d88581ce717e79f93 Mon Sep 17 00:00:00 2001 From: Paul Doyle Date: Fri, 10 Jan 2020 11:49:17 -0800 Subject: [PATCH 09/10] Adjust comments --- src/Component.lua | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Component.lua b/src/Component.lua index aed48ce3..1dfb5fb7 100644 --- a/src/Component.lua +++ b/src/Component.lua @@ -197,7 +197,8 @@ function Component:render() end --[[ - Retrieves the context object corresponding to the given key + Retrieves the context value corresponding to the given key. Can return nil + if a requested context key is not present ]] function Component:__getContext(key) if config.internalTypeChecks then @@ -212,8 +213,8 @@ function Component:__getContext(key) end --[[ - Adds new context property to this component's context table (which will be - passed down to child components) + Adds a new context entry to this component's context table (which will be + passed down to child components). ]] function Component:__addContext(key, value) if config.internalTypeChecks then @@ -221,15 +222,16 @@ function Component:__addContext(key, value) end local virtualNode = self[InternalData].virtualNode - -- If we don't already have the component's original, unmodified context - -- stored in the virtual node, store it now so we can restore it if the node - -- gets replaced by a different component + -- Make sure we store a reference to the component's original, unmodified + -- context the virtual node. In the reconciler, we'll restore the original + -- context if we need to replace the node (this happens when a node gets + -- re-rendered as a different component) if virtualNode.originalContext == nil then virtualNode.originalContext = virtualNode.context end - -- Build a new context table, on top of the existing one, and apply it to - -- our node + -- Build a new context table on top of the existing one, then apply it to + -- our virtualNode local existing = virtualNode.context virtualNode.context = assign({}, existing, { [key] = value }) end From 28cc872d561186c47345b2dd0b234f7edf4a8524 Mon Sep 17 00:00:00 2001 From: Paul Doyle Date: Fri, 10 Jan 2020 11:52:10 -0800 Subject: [PATCH 10/10] Fix luacheck warning --- src/RobloxRenderer.spec.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/src/RobloxRenderer.spec.lua b/src/RobloxRenderer.spec.lua index 9bbf2e7d..1ea04cb2 100644 --- a/src/RobloxRenderer.spec.lua +++ b/src/RobloxRenderer.spec.lua @@ -946,5 +946,4 @@ return function() }) end) end) - end \ No newline at end of file