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

Implement defaultProps #79

Merged
merged 10 commits into from
May 3, 2018
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Added `Roact.Change` for subscribing to `GetPropertyChangedSignal` ([#51](https://github.com/Roblox/roact/pull/51))
* 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))

## 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
7 changes: 7 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,13 @@ See [the Portals guide](/advanced/portals.md) for a small tutorial and more deta

## Component API

### defaultProps
```
static defaultProps: Dictionary<any, any>
```

If `defaultProps` is defined on a stateful component, any props that aren't specified when a component is created will be taken from there.

### init
```
init(initialProps) -> void
Expand Down
62 changes: 45 additions & 17 deletions lib/Component.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,29 @@ local tick = tick

Component.__index = Component

local function mergeState(currentState, partialState)
local newState = {}
--[[
Merge any number of dictionaries into a new dictionary, overwriting keys.

for key, value in pairs(currentState) do
newState[key] = value
end
If a value of `Core.None` is encountered, the key will be removed instead.
This is necessary because Lua doesn't differentiate between a key being
missing and a key being set to nil.
]]
local function merge(...)
local result = {}

for key, value in pairs(partialState) do
if value == Core.None then
newState[key] = nil
else
newState[key] = value
for i = 1, select("#", ...) do
local entry = select(i, ...)

for key, value in pairs(entry) do
if value == Core.None then
result[key] = nil
else
result[key] = value
end
end
end

return newState
return result
end

--[[
Expand Down Expand Up @@ -85,7 +92,12 @@ function Component:extend(name)
-- You can see a list of reasons in invalidSetStateMessages.
self._setStateBlockedReason = nil

self.props = props
if class.defaultProps == nil then
self.props = props
else
self.props = merge(class.defaultProps, props)
end

self._context = {}

-- Shallow copy all context values from our parent element.
Expand Down Expand Up @@ -113,7 +125,7 @@ function Component:extend(name)
local partialState = class.getDerivedStateFromProps(props, self.state)

if partialState then
self.state = mergeState(self.state, partialState)
self.state = merge(self.state, partialState)
end
end

Expand Down Expand Up @@ -223,7 +235,7 @@ function Component:setState(partialState)
end
end

local newState = mergeState(self.state, partialState)
local newState = merge(self.state, partialState)
self:_update(self.props, newState)
end

Expand Down Expand Up @@ -268,15 +280,31 @@ function Component:_forceUpdate(newProps, newState)
-- Get the class - getDerivedStateFromProps is static.
local class = getmetatable(self)

-- If newProps was provided, we should make sure any derived state we have
-- is updated before we continue.
-- If newProps are passed, compute derived state and default props
if newProps then
if class.getDerivedStateFromProps then
local derivedState = class.getDerivedStateFromProps(newProps, newState or self.state)

-- getDerivedStateFromProps can return nil if no changes are necessary.
if derivedState ~= nil then
newState = mergeState(newState or self.state, derivedState)
newState = merge(newState or self.state, derivedState)
end
end

if class.defaultProps then
-- We only allocate another prop table if there are props that are
-- falling back to their default.
local replacementProps

for key in pairs(class.defaultProps) do
if newProps[key] == nil then
replacementProps = merge(class.defaultProps, newProps)
break
end
end

if replacementProps then
newProps = replacementProps
end
end
end
Expand Down
76 changes: 76 additions & 0 deletions lib/Component.spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,82 @@ return function()
Reconciler.teardown(handle)
end)

it("should pull values from defaultProps where appropriate", function()
local lastProps
local TestComponent = Component:extend("TestComponent")

TestComponent.defaultProps = {
foo = "hello",
bar = "world",
}

function TestComponent:render()
lastProps = self.props
return nil
end

local handle = Reconciler.reify(Core.createElement(TestComponent))

expect(lastProps).to.be.a("table")
expect(lastProps.foo).to.equal("hello")
expect(lastProps.bar).to.equal("world")

Reconciler.teardown(handle)

lastProps = nil
handle = Reconciler.reify(Core.createElement(TestComponent, {
foo = 5,
}))

expect(lastProps).to.be.a("table")
expect(lastProps.foo).to.equal(5)
expect(lastProps.bar).to.equal("world")

Reconciler.teardown(handle)

lastProps = nil
handle = Reconciler.reify(Core.createElement(TestComponent, {
bar = false,
}))

expect(lastProps).to.be.a("table")
expect(lastProps.foo).to.equal("hello")
expect(lastProps.bar).to.equal(false)

Reconciler.teardown(handle)
end)

it("should fall back to defaultProps correctly after an update", function()
local lastProps
local TestComponent = Component:extend("TestComponent")

TestComponent.defaultProps = {
foo = "hello",
bar = "world",
}

function TestComponent:render()
lastProps = self.props
return nil
end

local handle = Reconciler.reify(Core.createElement(TestComponent, {
foo = "hey"
}))

expect(lastProps).to.be.a("table")
expect(lastProps.foo).to.equal("hey")
expect(lastProps.bar).to.equal("world")

handle = Reconciler.reconcile(handle, Core.createElement(TestComponent))

expect(lastProps).to.be.a("table")
expect(lastProps.foo).to.equal("hello")
expect(lastProps.bar).to.equal("world")

Reconciler.teardown(handle)
end)

describe("setState", function()
it("should throw when called in init", function()
local InitComponent = Component:extend("InitComponent")
Expand Down