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 7 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
53 changes: 53 additions & 0 deletions docs/advanced/refs.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,59 @@
To create a ref, pass a function prop with the key `Roact.Ref` when creating a primitive element.

For example, suppose we wanted to create a search bar that captured cursor focus when any part of it was clicked. We might use a component like this:
```lua
--[[
A search bar with an icon and a text box that captures focus for its TextBox
when its icon is clicked
]]
local SearchBar = Roact.Component:extend("SearchBar")

function SearchBar:init()
-- Roact.createRef creates an object reference.
-- This has a single property, `current`, that can be used to access the
-- current Roblox instance.
self.textBoxRef = Roact.createRef()
end

function SearchBar:captureFocus()
local textBox = self.textBoxRef.current

-- If we have a current instance, capture focus on it.
-- current will be nil if the component hasn't been mounted yet, or it's
-- being unmounted.
if textBox then
textBox:CaptureFocus()
end
end

function SearchBar:render()
-- Render our icon and text box side by side in a Frame
return Roact.createElement("Frame", {
Size = UDim2.new(0, 200, 0, 20),
}, {
SearchIcon = Roact.createElement("ImageButton", {
Size = UDim2.new(0, 20, 0, 20),
-- Handle click events on the icon
[Roact.Event.MouseButton1Click] = function()
self:captureFocus()
end,
}),

SearchTextBox = Roact.createElement("TextBox", {
Size = UDim2.new(0, 180, 0, 20),
Position = UDim2.new(0, 20, 0, 0),
-- We use Roact.Ref to get a reference to the underlying object
-- Roact will set textBoxRef.current to the underlying object as
-- part of the rendering process.
[Roact.Ref] = self.textBoxRef,
}),
})
end
```
When a user clicks on the outer `ImageButton`, the `captureFocus` method will be called and the `TextBox` instance will get focus as if it had been clicked on directly.

## Functional Refs
Roact allows you to use functions as refs. The function will be called with the Roblox object that Roact creates. For example, this is the SearchBar component from above, modified to use functional refs instead of object refs:
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 move the guie changes to a new PR? There are quite a few more things we need to catch across the docs, and I want to let the API simmer a bit before recommending it as the new default.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair enough, done!

```lua
--[[
A search bar with an icon and a text box that captures focus for its TextBox
Expand Down Expand Up @@ -51,6 +103,7 @@ end
When a user clicks on the outer `ImageButton`, the `captureTextboxFocus` callback will be triggered and the `TextBox` instance will get focus as if it had been clicked on directly.

## Refs During Teardown

!!! warning
When a component instance is destroyed or the ref property changes, `nil` will be passed to the old ref function!

Expand Down
11 changes: 11 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 @@ -72,6 +79,10 @@ If you're writing a new functional or stateful element that needs to be used lik
Use `Roact.Ref` as a key into the props of a primitive element to receive a handle to the underlying Roblox Instance.

```lua
Roact.createElement("Frame", {
[Roact.Ref] = objectReference,
})
Copy link
Contributor

Choose a reason for hiding this comment

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

This example seems sort of incomplete -- we should break the functional and object ref examples into two separate blocks and label what they do!


Roact.createElement("Frame", {
[Roact.Ref] = function(rbx)
print("Roblox Instance", rbx)
Expand Down
25 changes: 20 additions & 5 deletions lib/Reconciler.lua
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ 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 type(ref) == "table" then
ref.current = newRbx
else
ref(newRbx)
end
end

local Reconciler = {}

Reconciler._singleEventManager = SingleEventManager.new()
Expand Down Expand Up @@ -93,8 +105,10 @@ 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)
local ref = element.props[Core.Ref]

if ref then
applyRef(ref, nil)
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's move this condition into applyRef itself, which would make all of these invocations a bit cleaner!

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'd help clean some stuff up, yeah!

end

for _, child in pairs(instanceHandle._children) do
Expand Down Expand Up @@ -173,8 +187,9 @@ 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)
local ref = element.props[Core.Ref]
if ref then
applyRef(ref, rbx)
end

return {
Expand Down Expand Up @@ -323,7 +338,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(%q)"):format(tostring(self.current))
Copy link
Contributor

Choose a reason for hiding this comment

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

Thinking about it now, do you think we should just format these with %s instead of %q? Since they'll mostly be Roblox objects, I don't know if it makes much sense to quote them!

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 true - I'm used to quoting everything, but in this case it doesn't do much good! Fixed now.

end,
}

return function()
return setmetatable({
current = nil,
}, refMetatable)
end
12 changes: 12 additions & 0 deletions lib/createRef.spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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\")")
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