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

createRef API #92

Merged
merged 19 commits into from
May 16, 2018
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
26 changes: 19 additions & 7 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 @@ -323,7 +335,7 @@ function Reconciler._reconcileInternal(instanceHandle, newElement)

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to update the branch on line 349 that applies newRef as well! It's calling newRef as a function.

end

-- Update properties and children of the Roblox object.
Expand Down
31 changes: 31 additions & 0 deletions lib/Reconciler.spec.lua
Original file line number Diff line number Diff line change
@@ -1,8 +1,39 @@
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a test that makes sure that refs can be unattached and attached through Reconciler.reconcile?

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're going to test tostring on these, let's also verify that they take on the name of their current value if set, too!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair, done!

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