Skip to content
This repository has been archived by the owner on Dec 13, 2023. It is now read-only.

Commit

Permalink
createRef API (#92)
Browse files Browse the repository at this point in the history
  • Loading branch information
AmaranthineCodices authored and LPGhatguy committed May 16, 2018
1 parent 146cb34 commit b25a063
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* Added the static lifecycle method `getDerivedStateFromProps` ([#57](https://github.com/Roblox/roact/pull/57))
* Allow canceling render by returning nil from setState callback ([#64](https://github.com/Roblox/roact/pull/64))
* Added `defaultProps` value on stateful components to define values for props that aren't specified ([#79](https://github.com/Roblox/roact/pull/79))
* Added `createRef` ([#70](https://github.com/Roblox/roact/issues/70), [#92](https://github.com/Roblox/roact/pull/92))

## 1.0.0 Prerelease 2 (March 22, 2018)
* Removed `is*Element` methods, this is unlikely to affect anyone ([#50](https://github.com/Roblox/roact/pull/50))
Expand Down
31 changes: 31 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ If `children` contains more than one child, `oneChild` function will throw an er

If `children` is `nil` or contains no children, `oneChild` will return `nil`.

### Roact.createRef
```
Roact.createRef() -> Ref
```

Creates a new reference object that can be used with [Roact.Ref](#roactref).

## Constants

### Roact.Children
Expand All @@ -71,14 +78,38 @@ If you're writing a new functional or stateful element that needs to be used lik
### Roact.Ref
Use `Roact.Ref` as a key into the props of a primitive element to receive a handle to the underlying Roblox Instance.

`Ref` may either be a function:
```lua
Roact.createElement("Frame", {
-- The function given will be called whenever the rendered instance changes.
[Roact.Ref] = function(rbx)
print("Roblox Instance", rbx)
end,
})
```

Or a reference object created with [createRef](#roactcreateref):
```lua
local ExampleComponent = Roact.Component:extend("ExampleComponent")

function ExampleComponent:init()
-- Create a reference object.
self.ref = Roact.createRef()
end

function ExampleComponent:render()
return Roact.createElement("Frame", {
-- Use the reference object to point to this rendered instance.
[Roact.Ref] = ref,
})
end

function ExampleComponent:didMount()
-- Access the current value of a reference object using its current property.
print("Roblox Instance", self.ref.current)
end
```

!!! warning
`Roact.Ref` will be called with `nil` when the component instance is destroyed!

Expand Down
32 changes: 22 additions & 10 deletions lib/Reconciler.lua
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,22 @@ local function isPortal(element)
return element.component == Core.Portal
end

--[[
Sets the value of a reference to a new rendered object.
Correctly handles both function-style and object-style refs.
]]
local function applyRef(ref, newRbx)
if ref == nil then
return
end

if type(ref) == "table" then
ref.current = newRbx
else
ref(newRbx)
end
end

local Reconciler = {}

Reconciler._singleEventManager = SingleEventManager.new()
Expand Down Expand Up @@ -93,9 +109,7 @@ function Reconciler.unmount(instanceHandle)

-- Kill refs before we make changes, since any mutations past this point
-- aren't relevant to components.
if element.props[Core.Ref] then
element.props[Core.Ref](nil)
end
applyRef(element.props[Core.Ref], nil)

for _, child in pairs(instanceHandle._children) do
Reconciler.unmount(child)
Expand Down Expand Up @@ -173,9 +187,7 @@ function Reconciler._mountInternal(element, parent, key, context)
rbx.Parent = parent

-- Attach ref values, since the instance is initialized now.
if element.props[Core.Ref] then
element.props[Core.Ref](rbx)
end
applyRef(element.props[Core.Ref], rbx)

return {
[isInstanceHandle] = true,
Expand Down Expand Up @@ -317,13 +329,13 @@ function Reconciler._reconcileInternal(instanceHandle, newElement)
if isPrimitiveElement(newElement) then
-- Roblox Instance change

local oldRef = oldElement[Core.Ref]
local newRef = newElement[Core.Ref]
local oldRef = oldElement.props[Core.Ref]
local newRef = newElement.props[Core.Ref]
local refChanged = (oldRef ~= newRef)

-- Cancel the old ref before we make changes. Apply the new one after.
if refChanged and oldRef then
oldRef(nil)
applyRef(oldRef, nil)
end

-- Update properties and children of the Roblox object.
Expand All @@ -334,7 +346,7 @@ function Reconciler._reconcileInternal(instanceHandle, newElement)

-- Apply the new ref if there was a ref change.
if refChanged and newRef then
newRef(instanceHandle._rbx)
applyRef(newRef, instanceHandle._rbx)
end

return instanceHandle
Expand Down
78 changes: 78 additions & 0 deletions lib/Reconciler.spec.lua
Original file line number Diff line number Diff line change
@@ -1,8 +1,86 @@
return function()
local Core = require(script.Parent.Core)
local Reconciler = require(script.Parent.Reconciler)
local createRef = require(script.Parent.createRef)

it("should mount booleans as nil", function()
local booleanReified = Reconciler.mount(false)
expect(booleanReified).to.never.be.ok()
end)

it("should handle object references properly", function()
local objectRef = createRef()
local element = Core.createElement("StringValue", {
[Core.Ref] = objectRef,
})

local handle = Reconciler.mount(element)
expect(objectRef.current).to.be.ok()
Reconciler.unmount(handle)
expect(objectRef.current).to.never.be.ok()
end)

it("should handle function references properly", function()
local currentRbx

local function ref(rbx)
currentRbx = rbx
end

local element = Core.createElement("StringValue", {
[Core.Ref] = ref,
})

local handle = Reconciler.mount(element)
expect(currentRbx).to.be.ok()
Reconciler.unmount(handle)
expect(currentRbx).to.never.be.ok()
end)

it("should handle changing function references", function()
local aValue, bValue

local function aRef(rbx)
aValue = rbx
end

local function bRef(rbx)
bValue = rbx
end

local element = Core.createElement("StringValue", {
[Core.Ref] = aRef,
})

local handle = Reconciler.mount(element, game, "Test123")
expect(aValue).to.be.ok()
expect(bValue).to.never.be.ok()
handle = Reconciler.reconcile(handle, Core.createElement("StringValue", {
[Core.Ref] = bRef,
}))
expect(aValue).to.never.be.ok()
expect(bValue).to.be.ok()
Reconciler.unmount(handle)
expect(bValue).to.never.be.ok()
end)

it("should handle changing object references", function()
local aRef = createRef()
local bRef = createRef()

local element = Core.createElement("StringValue", {
[Core.Ref] = aRef,
})

local handle = Reconciler.mount(element, game, "Test123")
expect(aRef.current).to.be.ok()
expect(bRef.current).to.never.be.ok()
handle = Reconciler.reconcile(handle, Core.createElement("StringValue", {
[Core.Ref] = bRef,
}))
expect(aRef.current).to.never.be.ok()
expect(bRef.current).to.be.ok()
Reconciler.unmount(handle)
expect(bRef.current).to.never.be.ok()
end)
end
20 changes: 20 additions & 0 deletions lib/createRef.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
--[[
Provides an API for acquiring a reference to a reified object. This
API is designed to mimic React 16.3's createRef API.
See:
* https://reactjs.org/docs/refs-and-the-dom.html
* https://reactjs.org/blog/2018/03/29/react-v-16-3.html#createref-api
]]

local refMetatable = {
__tostring = function(self)
return ("RoactReference(%s)"):format(tostring(self.current))
end,
}

return function()
return setmetatable({
current = nil,
}, refMetatable)
end
15 changes: 15 additions & 0 deletions lib/createRef.spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
return function()
local createRef = require(script.Parent.createRef)

it("should create refs", function()
expect(createRef()).to.be.ok()
end)

it("should support tostring on refs", function()
local ref = createRef()
expect(tostring(ref)).to.equal("RoactReference(nil)")

ref.current = "foo"
expect(tostring(ref)).to.equal("RoactReference(foo)")
end)
end
2 changes: 2 additions & 0 deletions lib/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

local Component = require(script.Component)
local Core = require(script.Core)
local createRef = require(script.createRef)
local Event = require(script.Event)
local Change = require(script.Change)
local GlobalConfig = require(script.GlobalConfig)
Expand Down Expand Up @@ -39,6 +40,7 @@ apply(Roact, ReconcilerCompat)

apply(Roact, {
Component = Component,
createRef = createRef,
PureComponent = PureComponent,
Event = Event,
Change = Change,
Expand Down
1 change: 1 addition & 0 deletions lib/init.spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ return function()
it("should load with all public APIs", function()
local publicApi = {
createElement = "function",
createRef = "function",
mount = "function",
unmount = "function",
reconcile = "function",
Expand Down

0 comments on commit b25a063

Please sign in to comment.