Skip to content

Commit a4212dc

Browse files
author
Brian Vaughn
committed
Support editable props, state, and context values
1 parent 6c226b0 commit a4212dc

15 files changed

Lines changed: 428 additions & 37 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.App {
2+
/* GitHub.com frontend fonts */
3+
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
4+
sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
5+
font-size: 14px;
6+
line-height: 1.5;
7+
}
8+
9+
.Header {
10+
font-size: 1.5rem;
11+
font-weight: bold;
12+
margin-bottom: 0.5rem;
13+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// @flow
2+
3+
import React, { createContext, Component, Fragment } from 'react';
4+
import styles from './EditableProps.css';
5+
6+
type StatefulFunctionProps = {| count: number |};
7+
8+
function StatefulFunction({ count }: StatefulFunctionProps) {
9+
return <li>Count: {count}</li>;
10+
}
11+
12+
const BoolContext = createContext(true);
13+
// $FlowFixMe Flow does not yet know about Context.displayName
14+
BoolContext.displayName = 'BoolContext';
15+
16+
type Props = {| name: string, toggle: boolean |};
17+
type State = {| cities: Array<string>, state: string |};
18+
19+
class StatefulClass extends Component<Props, State> {
20+
static contextType = BoolContext;
21+
22+
state: State = {
23+
cities: ['San Francisco', 'San Jose'],
24+
state: 'California',
25+
};
26+
27+
handleChange = ({ target }) =>
28+
this.setState({
29+
state: target.value,
30+
});
31+
32+
render() {
33+
return (
34+
<Fragment>
35+
<li>Name: {this.props.name}</li>
36+
<li>Toggle: {this.props.toggle ? 'true' : 'false'}</li>
37+
<li>
38+
State: <input value={this.state.state} onChange={this.handleChange} />
39+
</li>
40+
<li>Cities: {this.state.cities.join(', ')}</li>
41+
<li>Context: {this.context ? 'true' : 'false'}</li>
42+
</Fragment>
43+
);
44+
}
45+
}
46+
47+
export default function EditableProps() {
48+
return (
49+
<div className={styles.App}>
50+
<div className={styles.Header}>Editable props</div>
51+
<ul>
52+
<StatefulClass name="Brian" toggle={true} />
53+
<StatefulFunction count={1} />
54+
</ul>
55+
</div>
56+
);
57+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// @flow
2+
3+
import EditableProps from './EditableProps';
4+
5+
export default EditableProps;

shells/dev/app/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import { createElement } from 'react';
66
import { render, unmountComponentAtNode } from 'react-dom';
7+
import EditableProps from './EditableProps';
78
import ElementTypes from './ElementTypes';
89
import InspectableElements from './InspectableElements';
910
import ToDoList from './ToDoList';
@@ -24,6 +25,7 @@ function mountTestApp() {
2425
mountHelper(ToDoList);
2526
mountHelper(InspectableElements);
2627
mountHelper(ElementTypes);
28+
mountHelper(EditableProps);
2729
}
2830

2931
function unmountTestApp() {

src/backend/agent.js

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ const debug = (methodName, ...args) => {
1818
}
1919
};
2020

21+
type InspectSelectParams = {|
22+
id: number,
23+
rendererID: number,
24+
|};
25+
26+
type SetInParams = {|
27+
id: number,
28+
path: Array<string | number>,
29+
rendererID: number,
30+
value: any,
31+
|};
32+
2133
export default class Agent extends EventEmitter {
2234
_bridge: Bridge = ((null: any): Bridge);
2335
_rendererInterfaces: { [key: RendererID]: RendererInterface } = {};
@@ -27,6 +39,9 @@ export default class Agent extends EventEmitter {
2739

2840
bridge.addListener('highlightElementInDOM', this.highlightElementInDOM);
2941
bridge.addListener('inspectElement', this.inspectElement);
42+
bridge.addListener('overrideContext', this.overrideContext);
43+
bridge.addListener('overrideProps', this.overrideProps);
44+
bridge.addListener('overrideState', this.overrideState);
3045
bridge.addListener('selectElement', this.selectElement);
3146
bridge.addListener('startInspectingDOM', this.startInspectingDOM);
3247
bridge.addListener('stopInspectingDOM', this.stopInspectingDOM);
@@ -80,7 +95,7 @@ export default class Agent extends EventEmitter {
8095
}
8196
};
8297

83-
inspectElement = ({ id, rendererID }: { id: number, rendererID: number }) => {
98+
inspectElement = ({ id, rendererID }: InspectSelectParams) => {
8499
const renderer = this._rendererInterfaces[rendererID];
85100
if (renderer == null) {
86101
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
@@ -89,7 +104,7 @@ export default class Agent extends EventEmitter {
89104
}
90105
};
91106

92-
selectElement = ({ id, rendererID }: { id: number, rendererID: number }) => {
107+
selectElement = ({ id, rendererID }: InspectSelectParams) => {
93108
const renderer = this._rendererInterfaces[rendererID];
94109
if (renderer == null) {
95110
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
@@ -98,6 +113,33 @@ export default class Agent extends EventEmitter {
98113
}
99114
};
100115

116+
overrideContext = ({ id, path, rendererID, value }: SetInParams) => {
117+
const renderer = this._rendererInterfaces[rendererID];
118+
if (renderer == null) {
119+
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
120+
} else {
121+
renderer.setInContext(id, path, value);
122+
}
123+
};
124+
125+
overrideProps = ({ id, path, rendererID, value }: SetInParams) => {
126+
const renderer = this._rendererInterfaces[rendererID];
127+
if (renderer == null) {
128+
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
129+
} else {
130+
renderer.setInProps(id, path, value);
131+
}
132+
};
133+
134+
overrideState = ({ id, path, rendererID, value }: SetInParams) => {
135+
const renderer = this._rendererInterfaces[rendererID];
136+
if (renderer == null) {
137+
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
138+
} else {
139+
renderer.setInState(id, path, value);
140+
}
141+
};
142+
101143
setRendererInterface(
102144
rendererID: RendererID,
103145
rendererInterface: RendererInterface

src/backend/renderer.js

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import { gte } from 'semver';
44
import {
5-
ElementTypeClassOrFunction,
5+
ElementTypeClass,
6+
ElementTypeFunction,
67
ElementTypeContext,
78
ElementTypeForwardRef,
89
ElementTypeMemo,
@@ -12,7 +13,7 @@ import {
1213
ElementTypeSuspense,
1314
} from 'src/devtools/types';
1415
import { getDisplayName, utfEncodeString } from '../utils';
15-
import { cleanForBridge } from './utils';
16+
import { cleanForBridge, copyWithSet, setInObject } from './utils';
1617
import {
1718
__DEBUG__,
1819
TREE_OPERATION_ADD,
@@ -193,6 +194,8 @@ export function attach(
193194
DEPRECATED_PLACEHOLDER_SYMBOL_STRING,
194195
} = ReactSymbols;
195196

197+
const { overrideProps } = renderer;
198+
196199
const debug = (name: string, fiber: Fiber, parentFiber: ?Fiber): void => {
197200
if (__DEBUG__) {
198201
const fiberData = getDataForFiber(fiber);
@@ -295,13 +298,19 @@ export function attach(
295298

296299
switch (tag) {
297300
case ClassComponent:
298-
case FunctionComponent:
299301
case IncompleteClassComponent:
302+
fiberData = {
303+
displayName: getDisplayName(resolvedType),
304+
key,
305+
type: ElementTypeClass,
306+
};
307+
break;
308+
case FunctionComponent:
300309
case IndeterminateComponent:
301310
fiberData = {
302311
displayName: getDisplayName(resolvedType),
303312
key,
304-
type: ElementTypeClassOrFunction,
313+
type: ElementTypeFunction,
305314
};
306315
break;
307316
case ForwardRef:
@@ -1072,7 +1081,7 @@ export function attach(
10721081
tag === IncompleteClassComponent ||
10731082
tag === IndeterminateComponent
10741083
) {
1075-
if (stateNode && Object.keys(stateNode.context).length > 0) {
1084+
if (stateNode && stateNode.context != null) {
10761085
context = stateNode.context;
10771086
}
10781087
} else if (
@@ -1135,7 +1144,7 @@ export function attach(
11351144
id,
11361145

11371146
// Does the current renderer support editable props/state/hooks?
1138-
canEditValues: false, // TODO
1147+
canEditFunctionProps: typeof overrideProps === 'function',
11391148

11401149
// Inspectable properties.
11411150
// TODO Review sanitization approach for the below inspectable values.
@@ -1156,6 +1165,49 @@ export function attach(
11561165
};
11571166
}
11581167

1168+
function setInProps(id: number, path: Array<string | number>, value: any) {
1169+
const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id));
1170+
if (fiber !== null) {
1171+
const instance = fiber.stateNode;
1172+
if (instance === null) {
1173+
if (typeof overrideProps === 'function') {
1174+
overrideProps(fiber, path, value);
1175+
}
1176+
} else {
1177+
fiber.pendingProps = copyWithSet(instance.props, path, value);
1178+
instance.forceUpdate();
1179+
}
1180+
}
1181+
}
1182+
1183+
function setInState(id: number, path: Array<string | number>, value: any) {
1184+
const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id));
1185+
if (fiber !== null) {
1186+
const instance = fiber.stateNode;
1187+
setInObject(instance.state, path, value);
1188+
instance.forceUpdate();
1189+
}
1190+
}
1191+
1192+
function setInContext(id: number, path: Array<string | number>, value: any) {
1193+
// To simplify hydration and display of primative context values (e.g. number, string)
1194+
// the inspectElement() method wraps context in a {value: ...} object.
1195+
// We need to remove the first part of the path (the "value") before continuing.
1196+
path = path.slice(1);
1197+
1198+
const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id));
1199+
if (fiber !== null) {
1200+
const instance = fiber.stateNode;
1201+
if (path.length === 0) {
1202+
// Simple context value
1203+
instance.context = value;
1204+
} else {
1205+
setInObject(instance.context, path, value);
1206+
}
1207+
instance.forceUpdate();
1208+
}
1209+
}
1210+
11591211
return {
11601212
getFiberIDFromNative,
11611213
getNativeFromReactElement,
@@ -1164,7 +1216,10 @@ export function attach(
11641216
inspectElement,
11651217
selectElement,
11661218
cleanup,
1167-
walkTree,
11681219
renderer,
1220+
setInContext,
1221+
setInProps,
1222+
setInState,
1223+
walkTree,
11691224
};
11701225
}

src/backend/types.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ export type RendererInterface = {
5555
inspectElement: (id: number) => InspectedElement | null,
5656
renderer: ReactRenderer | null,
5757
selectElement: (id: number) => void,
58+
setInProps: (id: number, path: Array<string | number>, value: any) => void,
59+
setInState: (id: number, path: Array<string | number>, value: any) => void,
60+
setInContext: (id: number, path: Array<string | number>, value: any) => void,
5861
walkTree: () => void,
5962
};
6063

src/backend/utils.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,37 @@ export function cleanForBridge(data: Object | null): DehydratedData | null {
1616
return null;
1717
}
1818
}
19+
20+
export function copyWithSet(
21+
obj: Object | Array<any>,
22+
path: Array<string | number>,
23+
value: any,
24+
index: number = 0
25+
): Object | Array<any> {
26+
if (index >= path.length) {
27+
return value;
28+
}
29+
const key = path[index];
30+
const updated = Array.isArray(obj) ? obj.slice() : { ...obj };
31+
// $FlowFixMe number or string is fine here
32+
updated[key] = copyWithSet(obj[key], path, value, index + 1);
33+
return updated;
34+
}
35+
36+
export function setInObject(
37+
object: Object,
38+
path: Array<string | number>,
39+
value: any
40+
) {
41+
const last = path.pop();
42+
if (object != null) {
43+
const parent: Object = path.reduce(
44+
// $FlowFixMe
45+
(reduced, attribute) => reduced[attribute],
46+
object
47+
);
48+
if (parent) {
49+
parent[last] = value;
50+
}
51+
}
52+
}

src/devtools/types.js

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
// @flow
22

3-
export const ElementTypeClassOrFunction = 1;
4-
export const ElementTypeContext = 2;
5-
export const ElementTypeForwardRef = 3;
6-
export const ElementTypeMemo = 4;
7-
export const ElementTypeOtherOrUnknown = 5;
8-
export const ElementTypeProfiler = 6;
9-
export const ElementTypeRoot = 7;
10-
export const ElementTypeSuspense = 8;
3+
export const ElementTypeClass = 1;
4+
export const ElementTypeFunction = 2;
5+
export const ElementTypeContext = 3;
6+
export const ElementTypeForwardRef = 4;
7+
export const ElementTypeMemo = 5;
8+
export const ElementTypeOtherOrUnknown = 6;
9+
export const ElementTypeProfiler = 7;
10+
export const ElementTypeRoot = 8;
11+
export const ElementTypeSuspense = 9;
1112

1213
// Different types of elements displayed in the Elements tree.
1314
// These types may be used to visually distinguish types,
1415
// or to enable/disable certain functionality.
15-
export type ElementType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
16+
export type ElementType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
1617

1718
// Each element on the frontend corresponds to a Fiber on the backend.
1819
// Some of its information (e.g. id, type, displayName) come from the backend.
@@ -47,8 +48,8 @@ export type Owner = {|
4748
export type InspectedElement = {|
4849
id: number,
4950

50-
// Does the current renderer support editable props/state/hooks?
51-
canEditValues: boolean,
51+
// Does the current renderer support editable function props?
52+
canEditFunctionProps: boolean,
5253

5354
// Inspectable properties.
5455
context: Object | null,

0 commit comments

Comments
 (0)