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

Commit

Permalink
Merge 666d7a9 into 986c1d5
Browse files Browse the repository at this point in the history
  • Loading branch information
MisterUncloaked committed Aug 19, 2019
2 parents 986c1d5 + 666d7a9 commit e00057f
Show file tree
Hide file tree
Showing 12 changed files with 306 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Fixed a bug where derived state was lost when assigning directly to state in init ([#232](https://github.com/Roblox/roact/pull/232/))
* Improved the error message when an invalid changed hook name is used. ([#216](https://github.com/Roblox/roact/pull/216))
* Fixed a bug where fragments could not be used as children of an element or another fragment. ([#214](https://github.com/Roblox/roact/pull/214))
* Added Roact.Type, Roact.typeOf, and Roact.isComponent for Roact object and component type checking. ([#230](https://github.com/Roblox/roact/pull/230))

## [1.1.0](https://github.com/Roblox/roact/releases/tag/v1.1.0) (June 3rd, 2019)
* Fixed an issue where updating a host element with children to an element with `nil` children caused the old children to not be unmounted. ([#210](https://github.com/Roblox/roact/pull/210))
Expand Down
62 changes: 62 additions & 0 deletions docs/advanced/type-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
In certain situations, such as when building reusable and customizable components, props may be composed of Roact objects, such as an element or a component.

To facilitate safer development for these kinds of situations, Roact provides the `Roact.typeOf` and `Roact.isComponent` functions to help validate these objects.

## Roact Object Type Validation

Suppose we want to write a `Header` component with a prop for the title child element:
```lua
local Header = Component:extend("Header")

function Header:render()
local title = props.title

return Roact.createElement("Frame", {
-- Props for Frame...
}, {
Title = title
})
end
```

Now suppose we want to validate that `title` is actually an element using [validateProps](../../api-reference/#validateprops). With `Roact.typeOf` we can be certain we have a Roact Element:
```lua
Header.validateProps = function()
local title = props.title

if Roact.typeOf(title) == Roact.Type.Element then
return true
end

return false, "prop title is not an element"
end
```

## Component Type Validation

In some cases, a component will be more preferable as a prop than an element. `Roact.isComponent` can be used to see if a value is a plausible component and thus can be passed to `Roact.createElement`.

```lua
local Header = Component:extend("Header")

Header.validateProps = function()
local title = props.title

if Roact.isComponent(title) then
return true
end

return false, "prop title can not be an element"
end

function Header:render()
local title = props.title
return Roact.createElement("Frame", {
-- Props for Frame...
}, {
Title = Roact.isComponent(title) and Roact.createElement(title, {
-- Props for Title...
})
})
end
```
64 changes: 64 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,28 @@ end

---

### Roact.typeOf
<div class="api-addition">Added in 1.2.0</div>

```
Roact.typeOf(roactObject) -> Roact.Type
```

Returns the [Roact.Type](#roacttype) of the passed in Roact object, or `nil` if the input is not a Roact object.

---

### Roact.isComponent
<div class="api-addition">Added in 1.2.0</div>

```
Roact.isComponent(value) -> bool
```

Returns true is the provided value can be used by [Roact.createElement](#roactcreateelement).

---

### Roact.createRef
```
Roact.createRef() -> Ref
Expand Down Expand Up @@ -356,6 +378,48 @@ See [the Portals guide](../advanced/portals) for a small tutorial and more detai

---

## Enumerations

### Roact.Type
<div class="api-addition">Added in 1.2.0</div>

An enumeration of the various types of objects in Roact, returned from calling `Roact.typeOf` on Roact objects.

#### Roact.Type.Binding
`Roact.typeOf` object returned from `Roact.createBinding`

---

#### Roact.Type.Element
`Roact.typeOf` object returned from `Roact.createElement`

---

#### Roact.Type.HostChangeEvent
`Roact.typeOf` object returned when indexing into `Roact.Change`

---

#### Roact.Type.HostEvent
`Roact.typeOf` object returned when indexing into `Roact.Event`

---

#### Roact.Type.StatefulComponentClass
`Roact.typeOf` object returned from `Roact.Component:extend`

---

#### Roact.Type.StatefulComponentInstance
`Roact.typeOf` object of self inside of member methods of `Roact.Component`

---

#### Roact.Type.VirtualTree
`Roact.typeOf` object returned by `Roact.mount`

---

## Component API

### defaultProps
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ nav:
- Portals: advanced/portals.md
- Bindings and Refs: advanced/bindings-and-refs.md
- Context: advanced/context.md
- Type Validation: advanced/type-validation.md
- Performance Optimization:
- Overview: performance/overview.md
- Reduce Reconcilation: performance/reduce-reconciliation.md
Expand Down
14 changes: 13 additions & 1 deletion src/Type.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ local strict = require(script.Parent.strict)
local Type = newproxy(true)

local TypeInternal = {}
local TypeNames = {}

local function addType(name)
TypeInternal[name] = Symbol.named("Roact" .. name)
local symbol = Symbol.named("Roact" .. name)
TypeNames[symbol] = name
TypeInternal[name] = symbol
end

addType("Binding")
Expand All @@ -37,12 +40,21 @@ function TypeInternal.of(value)
return value[Type]
end

function TypeInternal.nameOf(type)
if typeof(type) ~= "userdata" then
return nil
end

return TypeNames[type]
end

getmetatable(Type).__index = TypeInternal

getmetatable(Type).__tostring = function()
return "RoactType"
end

strict(TypeInternal, "Type")
strict(TypeNames, "TypeNames")

return Type
4 changes: 4 additions & 0 deletions src/Type.spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,9 @@ return function()

expect(Type.of(test)).to.equal(Type.Element)
end)

it("should return a type's name", function()
expect(Type.nameOf(Type.Element)).to.equal("Element")
end)
end)
end
53 changes: 53 additions & 0 deletions src/TypeMirror.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
--[[
Mirrors a subset of values from Type.lua for external use, allowing
type checking on Roact objects without exposing internal Type symbols
TypeMirror: {
Type: Roact.Type,
typeOf: function(value: table) -> Roact.Type | nil
}
]]

local Type = require(script.Parent.Type)
local Symbol = require(script.Parent.Symbol)
local strict = require(script.Parent.strict)

local ALLOWED_TYPES = {
Type.Binding,
Type.Element,
Type.HostChangeEvent,
Type.HostEvent,
Type.StatefulComponentClass,
Type.StatefulComponentInstance,
Type.VirtualTree
}

local MirroredType = {}
for _, type in ipairs(ALLOWED_TYPES) do
local name = Type.nameOf(type)
MirroredType[name] = Symbol.named("Roact" .. name)
end

setmetatable(MirroredType, {
__tostring = function()
return "RoactType"
end
})

strict(MirroredType, "Type")

local Mirror = {
typeList = ALLOWED_TYPES,
Type = MirroredType,
typeOf = function(value)
local name = Type.nameOf(Type.of(value))
if not name then
return nil
end
return MirroredType[name]
end,
}

strict(Mirror, "TypeMirror")

return Mirror
53 changes: 53 additions & 0 deletions src/TypeMirror.spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
return function()
local Type = require(script.Parent.Type)
local Mirror = require(script.Parent.TypeMirror)

describe("Type", function()
it("should return a mirror of an internal type", function()
local name = Type.nameOf(Type.Element)
local mirroredType = Mirror.Type[name]
expect(mirroredType).to.equal(Mirror.Type.Element)
end)

it("should not return the actual internal type", function()
local name = Type.nameOf(Type.Element)
local mirroredType = Mirror.Type[name]
expect(mirroredType).to.never.equal(Type.Element)
end)

it("should include all allowed types", function()
for _, type in ipairs(Mirror.typeList) do
local name = Type.nameOf(type)
local mirroredType = Mirror.Type[name]
expect(mirroredType).to.be.ok()
end
end)

it("should not include any other types", function()
local name = Type.nameOf(Type.VirtualNode)
local success = pcall(function()
local _ = Mirror.Type[name]
end)
expect(success).to.equal(false)
end)
end)

describe("typeOf", function()
it("should return nil if the value is not a valid type", function()
expect(Mirror.typeOf(1)).to.equal(nil)
expect(Mirror.typeOf(true)).to.equal(nil)
expect(Mirror.typeOf"test").to.equal(nil)
expect(Mirror.typeOf(print)).to.equal(nil)
expect(Mirror.typeOf({})).to.equal(nil)
expect(Mirror.typeOf(newproxy(true))).to.equal(nil)
end)

it("should return the assigned type", function()
local test = {
[Type] = Type.Element
}

expect(Mirror.typeOf(test)).to.equal(Mirror.Type.Element)
end)
end)
end
5 changes: 5 additions & 0 deletions src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ local createReconcilerCompat = require(script.createReconcilerCompat)
local RobloxRenderer = require(script.RobloxRenderer)
local strict = require(script.strict)
local Binding = require(script.Binding)
local TypeMirror = require(script.TypeMirror)

local robloxReconciler = createReconciler(RobloxRenderer)
local reconcilerCompat = createReconcilerCompat(robloxReconciler)
Expand Down Expand Up @@ -37,6 +38,10 @@ local Roact = strict {
teardown = reconcilerCompat.teardown,
reconcile = reconcilerCompat.reconcile,

isComponent = require(script.isComponent),
typeOf = TypeMirror.typeOf,
Type = TypeMirror.Type,

setGlobalConfig = GlobalConfig.set,

-- APIs that may change in the future without warning
Expand Down
3 changes: 3 additions & 0 deletions src/init.spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ return function()
update = "function",
oneChild = "function",
setGlobalConfig = "function",
typeOf = "function",
isComponent = "function",

-- These functions are deprecated and throw warnings!
reify = "function",
Expand All @@ -26,6 +28,7 @@ return function()
Event = true,
Change = true,
Ref = true,
Type = true,
None = true,
UNSTABLE = true,
}
Expand Down
14 changes: 14 additions & 0 deletions src/isComponent.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
local Portal = require(script.Parent.Portal)
local Type = require(script.Parent.Type)

-- Returns true if the provided object can be used by Roact.createElement
return function(value)
local valueType = type(value)

local isComponentClass = Type.of(value) == Type.StatefulComponentClass
local isValidFunctionComponentType = valueType == "function"
local isValidHostType = valueType == "string"
local isPortal = value == Portal

return isComponentClass or isValidFunctionComponentType or isValidHostType or isPortal
end
Loading

0 comments on commit e00057f

Please sign in to comment.