-
Notifications
You must be signed in to change notification settings - Fork 143
Shallow wrapper #223
base: master
Are you sure you want to change the base?
Shallow wrapper #223
Changes from 37 commits
72c6921
2b7dde9
f57fa89
7ee2939
45b48da
8450335
a1f914f
17d8146
4183766
830c0b6
39eae67
16e1ba0
c81d22f
9b09be8
5944bf3
ae299a5
60b18f2
2773da6
edb1ca3
d73ebf1
f4844ac
a541ec4
8733795
1b8e6e8
0a187d9
a843b91
caca4ed
f1da1a2
8dcd096
6bd217e
90a0e40
10c3041
846ffd9
764c00a
a36239c
7ef84d3
dd5bc30
3230ad4
8986ce0
9324979
5efa96c
342bbec
750f669
12539ec
f1dac37
cfd055a
4bb8234
6d677c3
89b3a8e
7073e0d
2764cbc
3f44eab
0699e75
d5bcf88
90010ec
5b99367
27db6d9
1b1553d
ab59379
13edb92
ead931b
6dc2133
aec002f
4a83085
01529ba
9f366de
780fc4f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
local ReplicatedStorage = game:GetService("ReplicatedStorage") | ||
|
||
local Settings = require(script.Parent.Settings) | ||
|
||
local function SyncSnapshots(newSnapshots) | ||
local snapshotsFolder = ReplicatedStorage:FindFirstChild(Settings.SnapshotFolderName) | ||
|
||
if not snapshotsFolder then | ||
snapshotsFolder = Instance.new("Folder") | ||
snapshotsFolder.Name = Settings.SnapshotFolderName | ||
snapshotsFolder.Parent = ReplicatedStorage | ||
end | ||
|
||
for name, value in pairs(newSnapshots) do | ||
local snapshot = Instance.new("ModuleScript") | ||
snapshot.Name = name | ||
snapshot.Source = value | ||
snapshot.Parent = snapshotsFolder | ||
end | ||
end | ||
|
||
local function PluginEditMode(plugin) | ||
local isPluginDeactivated = false | ||
|
||
plugin.Deactivation:Connect(function() | ||
isPluginDeactivated = true | ||
end) | ||
|
||
while not isPluginDeactivated do | ||
local newSnapshots = plugin:GetSetting(Settings.PluginSettingName) | ||
|
||
if newSnapshots then | ||
SyncSnapshots(newSnapshots) | ||
plugin:SetSetting(Settings.PluginSettingName, false) | ||
end | ||
|
||
wait(Settings.SyncDelay) | ||
end | ||
end | ||
|
||
return PluginEditMode |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
local ReplicatedStorage = game:GetService("ReplicatedStorage") | ||
|
||
local Settings = require(script.Parent.Settings) | ||
|
||
local function PluginRunMode(plugin) | ||
plugin.Unloading:Connect(function() | ||
local snapshotsFolder = ReplicatedStorage:FindFirstChild(Settings.SnapshotFolderName) | ||
|
||
local newSnapshots = {} | ||
|
||
if not snapshotsFolder then | ||
return | ||
end | ||
|
||
for _, snapshot in pairs(snapshotsFolder:GetChildren()) do | ||
if snapshot:IsA("StringValue") then | ||
newSnapshots[snapshot.Name] = snapshot.Value | ||
end | ||
end | ||
|
||
plugin:SetSetting(Settings.PluginSettingName, newSnapshots) | ||
end) | ||
end | ||
|
||
return PluginRunMode |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
local function IndexError(_, key) | ||
local message = ("%q (%s) is not a valid member of Settings"):format( | ||
tostring(key), | ||
typeof(key) | ||
) | ||
|
||
error(message, 2) | ||
end | ||
|
||
return setmetatable({ | ||
SnapshotFolderName = "RoactSnapshots", | ||
PluginSettingName = "NewRoactSnapshots", | ||
SyncDelay = 1, | ||
}, { | ||
__index = IndexError, | ||
__newindex = IndexError, | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
local RunService = game:GetService("RunService") | ||
|
||
local EditModeMain = require(script.EditModeMain) | ||
local RunModeMain = require(script.RunModeMain) | ||
|
||
if RunService:IsEdit() then | ||
EditModeMain(plugin) | ||
else | ||
if RunService:IsClient() then | ||
RunModeMain(plugin) | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,21 +32,21 @@ Creates a new Roact fragment with the provided table of elements. Fragments allo | |
|
||
### Roact.mount | ||
``` | ||
Roact.mount(element, [parent, [key]]) -> RoactTree | ||
Roact.mount(element, [parent, [key]]) -> VirtualTree | ||
``` | ||
|
||
!!! info | ||
`Roact.mount` is also available via the deprecated alias `Roact.reify`. It will be removed in a future release. | ||
|
||
Creates a Roblox Instance given a Roact element, and optionally a `parent` to put it in, and a `key` to use as the instance's `Name`. | ||
|
||
The result is a `RoactTree`, which is an opaque handle that represents a tree of components owned by Roact. You can pass this to APIs like `Roact.unmount`. It'll also be used for future debugging APIs. | ||
The result is a `VirtualTree`, which is an opaque handle that represents a tree of components owned by Roact. You can pass this to APIs like `Roact.unmount`. It'll also be used for future debugging APIs. | ||
|
||
--- | ||
|
||
### Roact.update | ||
``` | ||
Roact.update(tree, element) -> RoactTree | ||
Roact.update(tree, element) -> VirtualTree | ||
``` | ||
|
||
!!! info | ||
|
@@ -56,7 +56,7 @@ Updates an existing instance handle with a new element, returning a new handle. | |
|
||
`update` can be used to change the props of a component instance created with `mount` and is useful for putting Roact content into non-Roact applications. | ||
|
||
As of Roact 1.0, the returned `RoactTree` object will always be the same value as the one passed in. | ||
As of Roact 1.0, the returned `VirtualTree` object will always be the same value as the one passed in. | ||
|
||
--- | ||
|
||
|
@@ -68,7 +68,7 @@ Roact.unmount(tree) -> void | |
!!! info | ||
`Roact.unmount` is also available via the deprecated alias `Roact.teardown`. It will be removed in a future release. | ||
|
||
Destroys the given `RoactTree` and all of its descendants. Does not operate on a Roblox Instance -- this must be given a handle that was returned by `Roact.mount`. | ||
Destroys the given `VirtualTree` and all of its descendants. Does not operate on a Roblox Instance -- this must be given a handle that was returned by `Roact.mount`. | ||
|
||
--- | ||
|
||
|
@@ -608,4 +608,159 @@ end | |
As with `setState`, you can set use the constant `Roact.None` to remove a field from the state. | ||
|
||
!!! note | ||
`getDerivedStateFromProps` is a *static* lifecycle method. It does not have access to `self`, and must be a pure function. | ||
`getDerivedStateFromProps` is a *static* lifecycle method. It does not have access to `self`, and must be a pure function. | ||
|
||
--- | ||
|
||
## ShallowWrapper | ||
|
||
### Fields | ||
|
||
#### type | ||
``` | ||
type: { | ||
kind: ElementKind | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. like with #227 we might want a mirror and not expose the ElementKind symbols directly, so Elements cannot be impersonated. |
||
} | ||
``` | ||
|
||
The type dictionary always has the `kind` field that tell the component type. Additionally, depending of the kind of component, other information can be included. | ||
|
||
| kind | fields | description | | ||
| --- | --- | --- | | ||
| Host | className: string | the ClassName of the instance | | ||
| Function | functionComponent: function | the function that renders the element | | ||
| Stateful | component: table | the class-like table used to render the element | | ||
ZoteTheMighty marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
--- | ||
|
||
#### props | ||
The props of the ShallowWrapper. | ||
|
||
!!! note | ||
This dictionary will not contain the `Roact.Children` prop. To obtain the children elements wrapped into a ShallowWrapper, use the method `getChildren()` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be simpler to just populate children as a property of ShallowWrapper when we call new? I'm trying to think of when you want a ShallowWrapper but wouldn't call getChildren() There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should I just make a |
||
|
||
--- | ||
|
||
#### hostKey | ||
The `hostKey` that is used to map the element to it's parent. | ||
|
||
--- | ||
|
||
### Methods | ||
|
||
#### childrenCount | ||
``` | ||
childrenCount() -> number | ||
``` | ||
Returns the amount of children that the current ShallowWrapper contains. | ||
|
||
--- | ||
|
||
#### find | ||
``` | ||
find([constraints]) -> list[ShallowWrapper] | ||
``` | ||
When a dictionary of constraints is provided, the function will filter the children that do not satisfy all given constraints. Otherwise, as `getChildren` do, it returns a list of all children wrapped into ShallowWrappers. | ||
|
||
--- | ||
|
||
#### findUnique | ||
``` | ||
findUnique([constraints]) -> ShallowWrapper | ||
``` | ||
Similar to `find`, this method will assert that only one child satisfies the given constraints, or in the case where none is provided, will assert that there is simply only one child. | ||
|
||
--- | ||
|
||
#### getChildren | ||
``` | ||
getChildren() -> list[ShallowWrapper] | ||
``` | ||
Returns a list of all children wrapped into ShallowWrappers. | ||
|
||
--- | ||
|
||
#### getInstance | ||
``` | ||
getInstance() -> Instance or nil | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need getInstance? tostring + matchsnapshot + supporting methods to query the public fields of the shallow tree should be enough There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For more robust tests it can be interesting to directly assert on the instance. For example you can assert for a specific value of AbsoluteSize or AbsolutePosition. It could also useful if you want to test behavior from lifecycle methods that act on a ref. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In such a test, could you just use a ref? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there is already a Ref, yes because you can access it through the props. Otherwise I don't think so |
||
``` | ||
Returns the instance object associated with the ShallowWrapper. It can return `nil` if the component wrapped by the ShallowWrapper does not render an instance, but rather another component. Here is an example: | ||
|
||
```lua | ||
local function CoolComponent(props) | ||
return Roact.createElement("TextLabel") | ||
end | ||
|
||
local function MainComponent(props) | ||
return Roact.createElement("Frame", {}, { | ||
Cool = Roact.createElement(CoolComponent), | ||
}) | ||
end | ||
``` | ||
|
||
If we shallow-render the `MainComponent`, the default depth will not render the CoolComponent. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems like an explanation of how render depth works, not an explanation of what There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wanted to explain when |
||
|
||
```lua | ||
local element = Roact.createElement(MainComponent) | ||
|
||
local tree = Roact.mount(element) | ||
|
||
local wrapper = tree:getShallowWrapper() | ||
|
||
print(wrapper:getInstance() == nil) -- prints false | ||
|
||
local coolWrapper = wrapper:findUnique() | ||
|
||
print(coolWrapper:getInstance() == nil) -- prints true | ||
``` | ||
|
||
--- | ||
|
||
#### matchSnapshot | ||
``` | ||
matchSnapshot(identifier) | ||
``` | ||
If no previous snapshot with the given identifier exists, it will create a new StringValue instance that will contain Lua code representing the current ShallowWrapper. When an existing snapshot is found (a ModuleScript named as the provided identifier), it will require the ModuleScript and load the data from it. Then, if the loaded data is different from the current ShallowWrapper, an error will be thrown. | ||
|
||
!!! note | ||
As mentionned, `matchSnapshot` will create a StringValue, named like the given identifier, in which the generated lua code will be assigned to the Value property. When these values are generated in Studio during run mode, it's important to copy back the values and convert them into ModuleScripts. | ||
|
||
--- | ||
|
||
#### snapshotToString | ||
``` | ||
snapshotToString() -> string | ||
``` | ||
Returns the string source of the snapshot. Useful for debugging purposes. | ||
|
||
--- | ||
|
||
##### Constraints | ||
Constraints are passed through a dictionary that maps a constraint name to it's value. | ||
|
||
| name | value type | description | | ||
| --- | --- | --- | | ||
| kind | ElementKind | Filters with the ElementKind of the rendered elements | | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we need API references in this file for ElementKind since we are exposing it now as public for these constraint functions? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes if we don't expose ElementKind we should remove that! |
||
| className | string | Filters children that renders to host instance of the given class name | | ||
| component | string, function or table | Filter children from their components, by finding the functional component original function, the sub-class table of Roact.Component or from the class name of the instance rendered | | ||
| props | dictionary | Filters elements that contains at least the given set of props | | ||
| hostKey | string | Filters elements by the host key used | | ||
|
||
--- | ||
|
||
## VirtualTree | ||
|
||
### Methods | ||
|
||
#### getShallowWrapper | ||
``` | ||
getShallowWrapper(options) -> ShallowWrapper | ||
``` | ||
Options: | ||
```lua | ||
{ | ||
depth: number -- default to 1 | ||
} | ||
``` | ||
|
||
Wraps the current tree into a ShallowWrapper. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"name": "Snapshots Plugin", | ||
"tree": { | ||
"$path": "SnapshotsPlugin" | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know if I agree with changing the terminology here. Ideally, we should expose as little internal vocabulary as possible in order to define what snapshots are and what they do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can switch it back to RoactTree, it's just that the Type we are going to expose in #230 is called
VirtualTree
. So maybe it could be renamed in that other PR?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good question! I didn't realize we were exposing it in that PR. We should probably discuss this with @LPGhatguy.
My thoughts are still that
VirtualTree
probably means less to users, but maybe there's another better alternative too.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I agree that the term VirtualTree might be questionable to users, we should probably solves this in #230 if it's about to be merged first.