Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Formatic 3000 #124

Open
wants to merge 44 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
230f394
Draft of futureland spec
jdeal Apr 6, 2019
8eec2ce
Add future demo page
jdeal Apr 6, 2019
cebd6fa
Add simple form example/test/code.
jdeal Apr 6, 2019
8f390f4
FormContainer works either as controlled/uncontrolled
jdeal Apr 6, 2019
2c827b3
Rename some files
jdeal Apr 6, 2019
c5d0bc8
Fix test descriptions
jdeal Apr 6, 2019
6739955
Add FieldContainer example/test/code
jdeal Apr 6, 2019
f23a600
Add a built-in TextInput
jdeal Apr 6, 2019
3fe0ad1
Add built-in TextField per the spec
jdeal Apr 6, 2019
f193215
Cleanup tests
jdeal Apr 6, 2019
af6e3e0
Auto-generate unique input ids
jdeal Apr 6, 2019
0fa226e
Clean up test descriptions
jdeal Apr 6, 2019
067a500
Add support for renderTag
jdeal Apr 9, 2019
8878def
Fix linting errors
jdeal Apr 9, 2019
0f4c5d5
Remove reliance on symbols for renderKeys
jdeal Apr 9, 2019
0c571f3
Add accessibilty to problems/goals
jdeal Apr 9, 2019
018b2b6
Add support for renderComponent
jdeal Apr 10, 2019
221b69c
Split up monolithic index
jdeal Apr 10, 2019
2476fc3
Support nested values
jdeal Apr 10, 2019
ace4df5
Simplify UncontrolledContainer
jdeal Apr 11, 2019
565da6f
Remove private components from API
jdeal Apr 11, 2019
be8de3c
Make sure uncontrolled demo forms do not rerender
jdeal Apr 20, 2019
7e5f9bd
Check if `onChange` is a function before calling it
jdeal Apr 20, 2019
abe9f0c
Add failing test for avoiding rerendering
jdeal Apr 20, 2019
737daa6
Add ReactiveValueContainer/useReactiveValue primitives
jdeal Apr 24, 2019
dfc5f54
Use ReactiveValue to implement more performant useField
jdeal Apr 24, 2019
9cc81ac
Add IntegerField/IntegerInput
jdeal Apr 25, 2019
9218c37
Remove unused fields
jdeal Apr 25, 2019
c84bc32
Add AutoFields. Move field-components to their own folder.
Apr 27, 2019
6aeaf24
Remove AutoFields default in FormContainer
Apr 29, 2019
7d7a3ad
Better tests
Apr 29, 2019
9b6362d
Use RenderContext instead of ReactiveValue to get initial form value
Apr 29, 2019
0185cdd
Rename files/dirs
Apr 29, 2019
089c4f3
Merge pull request #126 from zapier/add-autofields
jdeal May 4, 2019
06c06b4
Allow using the whole value
jdeal May 4, 2019
4bc8326
Just a naming tweak
jdeal May 4, 2019
65f7c08
Even more performance ReactiveValue with metadata
jdeal May 4, 2019
87660c8
Use useReactiveValueMeta for AutoFields
jdeal May 4, 2019
a4a0242
Update meta when root value changes
May 9, 2019
4d3981b
Bump zapier-scripts to current
May 9, 2019
302cf96
Add jest-dom and convert ReactiveValue tests
May 9, 2019
0b80b23
Add eslint override to allow react-create-class
May 9, 2019
a08c04a
Ensure we have meta when updating
May 10, 2019
5e9b175
Merge pull request #127 from zapier/notify-meta-on-value
stevelikesmusic May 10, 2019
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
251 changes: 175 additions & 76 deletions src/future/ReactiveValue.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,104 +11,166 @@ function getPropertyValue(key, value) {
return undefined;
}

// Wrap a value so that we can subscribe to changes to specific properties of
// that value and ignore properties that don't change.
const createReactiveValue = defaultValue => {
let currentValue = defaultValue;
const listeners = [];
const listenersByKey = {};
function getTypeName(value) {
if (value === null) {
return 'null';
}
if (Array.isArray(value)) {
return 'array';
}
return typeof value;
}

function getValue() {
return currentValue;
class ReactiveValue {
constructor(defaultValue, key, parent) {
this.currentValue = defaultValue;
this.listeners = [];
this.key = key;
this.parent = parent;
this.children = {};
this.refCount = 0;
this.meta = null;
this.hasMetaChanged = false;
this.metaListeners = [];
}

function getValueAt(key) {
if (isObject(currentValue)) {
return currentValue[key];
}
getValueAt(key) {
return getPropertyValue(key, this.currentValue);
}

return undefined;
getValue() {
return this.currentValue;
}

function notifyForKey(key, newValueAtKey) {
listenersByKey[key].forEach(handler => {
handler(newValueAtKey);
});
// Get the property type of this value and the property types of its children.
getMeta() {
if (!this.meta) {
this.meta = {
type: getTypeName(this.currentValue),
propertyTypes: isObject(this.currentValue)
? Object.keys(this.currentValue).reduce((result, key) => {
result[key] = getTypeName(this.currentValue[key]);
return result;
}, {})
: {},
};
}
return this.meta;
}

// Set value at a key and notify all subscribers to that key and the root
// value.
function setValueAt(key, newValue) {
if (getValueAt(key) !== newValue) {
if (isObject(currentValue)) {
currentValue = {
...currentValue,
updateMeta() {
this.meta = null;
this.hasMetaChanged = true;
this.getMeta();
}

// Set property value and recurse up through parents.
setValueAt(key, newValue) {
if (newValue !== this.getValueAt(key)) {
if (isObject(this.currentValue)) {
this.currentValue = {
...this.currentValue,
[key]: newValue,
};
if (this.parent) {
this.parent.setValueAt(this.key, this.currentValue);
// Make sure our parent is actually holding the new value. If not,
// take the actual value from the parent.
this.currentValue = this.parent.getValueAt(this.key);
}
if (
this.meta &&
this.meta.propertyTypes[key] !== getTypeName(newValue)
) {
this.updateMeta();
}
}
// Notify of actual value, which could potentially be different from
// the desired value. For example, if the root value is not an object, or
// it's a proxy. At that point, we're pretty much into unsupported
// territory though, and the behavior is unknown.
const trueNewValue = getValueAt(key);
if (listenersByKey[key]) {
notifyForKey(key, trueNewValue);
}
// Notify any root value subscribers.
listeners.forEach(handler => {
handler(currentValue);
}
}

notifyUp(shouldNotifyParent = true) {
// Notify listeners for this value.
this.listeners.forEach(handler => {
handler(this.currentValue);
});
if (this.hasMetaChanged) {
this.metaListeners.forEach(handler => {
handler(this.getMeta());
});
this.hasMetaChanged = false;
}
// And maybe parent values.
if (this.parent && shouldNotifyParent) {
this.parent.notifyUp();
}
}

// Set the root value. Don't notify root listeners, since we don't want to
// cause a loop. But... if there are any listeners of properties, notify them
// if those properties have changed.
function setValue(newValue) {
if (newValue !== currentValue) {
const previousValue = currentValue;
currentValue = newValue;
for (const key in listenersByKey) {
const newValueAtKey = getPropertyValue(key, newValue);
if (getPropertyValue(key, previousValue) !== newValueAtKey) {
notifyForKey(key, newValueAtKey);
}
setValue(newValue, shouldNotifyParent = true) {
// Ignore this if it's the same value.
if (newValue !== this.currentValue) {
this.currentValue = newValue;
if (this.parent && shouldNotifyParent) {
this.parent.setValueAt(this.key, newValue);
// Make sure our parent is actually holding the new value. If not,
// take the actual value from the parent.
this.currentValue = this.parent.getValueAt(this.key);
}
// Set our child values, making sure they don't call us back since we
// already know.
for (const key in this.children) {
const child = this.children[key];
child.setValue(this.getValueAt(key), false);
}
// Notify our subscribers and our parent's subscribers.
this.notifyUp(shouldNotifyParent);
}
}

function subscribe(handler) {
listeners.push(handler);
subscribe(handler) {
this.listeners.push(handler);
return () => {
const i = listeners.indexOf(handler);
listeners.splice(i, 1);
const i = this.listeners.indexOf(handler);
this.listeners.splice(i, 1);
};
}

function subscribeAt(key, handler) {
if (!(key in listenersByKey)) {
listenersByKey[key] = [];
}
const listenersAtKey = listenersByKey[key];
listenersAtKey.push(handler);
subscribeMeta(handler) {
this.metaListeners.push(handler);
return () => {
const i = listenersAtKey.indexOf(handler);
listenersAtKey.splice(i, 1);
if (listenersAtKey.length === 0) {
delete listenersByKey[key];
}
const i = this.metaListeners.indexOf(handler);
this.metaListeners.splice(i, 1);
};
}

return {
getValue,
getValueAt,
setValue,
setValueAt,
subscribe,
subscribeAt,
};
};
// Return child, creating it if necessary, with a zero reference count.
getChild(key) {
if (!this.children[key]) {
this.children[key] = new ReactiveValue(this.getValueAt(key), key, this);
}
return this.children[key];
}

// Increment the reference count, and return a dispose function that will
// decrement the reference count and throw away the child when the reference
// count reaches zero.
hold() {
if (this.parent) {
this.refCount++;
return () => {
this.refCount--;
if (this.refCount === 0) {
this.parent.releaseAt(this.key);
}
};
}
return () => {};
}

// Dispose of a child.
releaseAt(key) {
delete this.children[key];
}
}

const ReactiveValueContext = React.createContext();

Expand All @@ -118,7 +180,7 @@ export function ReactiveValueContainer({ value, onChange, children }) {
const valueRef = useRef(null);
// Initialize our wrapper once.
if (valueRef.current === null) {
valueRef.current = createReactiveValue(value);
valueRef.current = new ReactiveValue(value);
}
// Any time we get a new value, set the wrapper's value to that value. The
// wrapper will ignore equal values, so no worries of a loop here.
Expand All @@ -141,23 +203,40 @@ export function ReactiveValueContainer({ value, onChange, children }) {
);
}

// Grabs a child and makes sure it gets disposed of when there are no more
// references to it.
function useChildReactiveValue(key) {
const parentReactiveValue = useContext(ReactiveValueContext);
const childReactiveValue = parentReactiveValue.getChild(key);
useEffect(
() => {
return childReactiveValue.hold();
},
[key]
);
return childReactiveValue;
}

// Get the current value and a setValue function for a particular property.
export function useReactiveValueAt(key) {
const reactiveValue = useContext(ReactiveValueContext);
const [value, setValue] = useState(() => reactiveValue.getValueAt(key));
const childReactiveValue = useChildReactiveValue(key);
const [value, setValue] = useState(() => childReactiveValue.getValue());
useEffect(
() => {
// Subscribe to changes to the property and set our value in state when
// that property changes.
return reactiveValue.subscribeAt(key, setValue);
return childReactiveValue.subscribe(setValue);
},
[key]
);
function setValueInContext(newValue) {
reactiveValue.setValueAt(key, newValue);
childReactiveValue.setValue(newValue);
}
return { value, setValue: setValueInContext };
}

// Get the current value and a setValue function for the current property (or
// root value).
export function useReactiveValue() {
const reactiveValue = useContext(ReactiveValueContext);
const [value, setValue] = useState(() => reactiveValue.getValue());
Expand All @@ -171,3 +250,23 @@ export function useReactiveValue() {
}
return { value, setValue: setValueInContext };
}

// Get the current property metadata.
export function useReactiveValueMeta() {
const reactiveValue = useContext(ReactiveValueContext);
const [meta, setMeta] = useState(() => reactiveValue.getMeta());
useEffect(() => {
return reactiveValue.subscribeMeta(setMeta);
}, []);
return meta;
}

// Nest into a child.
export function ReactiveChildContainer({ childKey, children }) {
const childReactiveValue = useChildReactiveValue(childKey);
return (
<ReactiveValueContext.Provider value={childReactiveValue}>
{children}
</ReactiveValueContext.Provider>
);
}
Loading