Skip to content

Commit 989d664

Browse files
initial implementation of withQuery()
1 parent 74f668d commit 989d664

15 files changed

+465
-40
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
### 0.1.0
2+
- Moved to withQuery() functionality
3+
14
### 0.0.5
25
- Improved stability
36

README.md

+220-27
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,234 @@
1-
Grapher-React Components
2-
========================
1+
## Grapher React Components
32

43
Using the [cultofcoders:grapher](https://github.com/cult-of-coders/grapher) query component in React.
54

6-
createQueryContainer
7-
--------------------
5+
### Installation
6+
```bash
7+
meteor add cultofcoders:grapher-react
8+
```
9+
10+
11+
### Signature
12+
813
```js
9-
// TaskList.jsx
10-
import React from 'react';
11-
import Tasks from '/imports/api/tasks/collection.js';
12-
import { createQueryContainer } from 'meteor/cultofcoders:grapher-react';
14+
withQuery(() => query, config)(Component)
15+
```
16+
17+
The first function needs to return a valid Query or NamedQuery from Grapher.
1318

14-
const query = Tasks.createQuery({
15-
title: 1,
16-
});
19+
### Configuration:
1720

18-
const TaskList = ({loading, error, tasks}) => (
19-
<div>
20-
{
21-
_.map(tasks, task => <div key={task._id}>{task.title}</div>)
21+
<table>
22+
<tr>
23+
<th>Property</th>
24+
<th>Valid values</th>
25+
<th>Description</th>
26+
</tr>
27+
<tr>
28+
<td>reactive</td>
29+
<td>true/false</td>
30+
<td>
31+
Defaults to `false`.
32+
Makes your query reactive (subscribes to changes) or non-reactive, falls back to method calls.
33+
</td>
34+
</tr>
35+
<tr>
36+
<td>errorComponent</td>
37+
<td>React.Component (optional)</td>
38+
<td>Defaults to `null`. Receives `error` object as a prop. Is rendered when subscription or method call triggered an exception</td>
39+
</tr>
40+
<tr>
41+
<td>loadingComponent</td>
42+
<td>React.Component (optional)</td>
43+
<td>Defaults to `null`. Renders when the data is waiting to be loaded from the server</td>
44+
</tr>
45+
<tr>
46+
<td>single</td>
47+
<td>true/false</td>
48+
<td>Defaults to `false`. If your query is for a single result, then using `true` will send data as an object instead of an array</td>
49+
</tr>
50+
</table>
51+
52+
### Simple Usage
53+
54+
```jsx harmony
55+
import React from 'react';
56+
import {withQuery} from 'meteor/cultofcoders:grapher-react';
57+
58+
const PostList = ({data, isLoading, error}) => {
59+
if (isLoading) {
60+
return <div>Loading</div>
61+
}
62+
63+
if (error) {
64+
return <div>{error.reason}</div>
2265
}
23-
</div>
24-
);
66+
67+
return (
68+
<div>
69+
{data.map(post => <li key={post._id}>{post.title}</li>)}
70+
</div>
71+
)
72+
}
2573

26-
export default createQueryContainer(query, TaskList, {
27-
reactive: true, // defaults to false, will use pub/sub system
28-
dataProp: 'tasks', // defaults to 'data'
29-
single: false, // defaults to false, when you expect a single document, like you filter by _id, use this.
30-
});
74+
export default withQuery((props) => {
75+
return getPostLists.clone();
76+
})(PostList)
3177
```
3278

33-
You can pass params directly in the constructor, these params will be passed to the query.
79+
### Props Received
3480

35-
```js
36-
import TaskList from './Tasks.jsx';
81+
Below are the properties received by the component we wrap, in the example above, that's `PostList`
82+
83+
<table>
84+
<tr>
85+
<th>Property</th>
86+
<th>Valid values</th>
87+
<th>Description</th>
88+
</tr>
89+
<tr>
90+
<td>isLoading</td>
91+
<td>true/false</td>
92+
<td>
93+
Lets your component know whether the data is waiting to be loaded.
94+
</td>
95+
</tr>
96+
<tr>
97+
<td>error</td>
98+
<td>Meteor.Error</td>
99+
<td>Represents the error triggered from your method or publication. Is falsy if it's not the case.</td>
100+
</tr>
101+
<tr>
102+
<td>refetch</td>
103+
<td>Function</td>
104+
<td>For non-reactive queries it passes a refetch function for convenience to help you easily reload the data.</td>
105+
</tr>
106+
<tr>
107+
<td>query</td>
108+
<td>Query/NamedQuery</td>
109+
<td>For your convenience, if you ever need the query for any reason, it's passed in there so you can access it.</td>
110+
</tr>
111+
<tr>
112+
<td>...props</td>
113+
<td></td>
114+
<td>The props you pass to withQuery, are passed down to the component it wraps</td>
115+
</tr>
116+
</table>
117+
118+
### Let's react!
119+
120+
The first example uses the query non-reactively (because that is the default). But let's say you want your query to be reactive (react to changes in the database)
121+
122+
```jsx harmony
123+
// ...
124+
export default withQuery((props) => {
125+
return getPostLists.clone();
126+
}, {reactive: true})(PostList)
127+
```
37128

38-
export default () => {
39-
return <TaskList params={{isActive: true}} anyOtherProp="willBePassedToComponent" />
129+
As mentioned above, the props received are passed down to the component we wrap, meaning:
130+
131+
```jsx harmony
132+
const PostList = ({data, something}) => {
133+
return <div>Something is true!</div>
134+
};
135+
136+
const Container = withQuery((props) => {
137+
return getPostLists.clone();
138+
}, {reactive: true})(PostList);
139+
140+
export default function () {
141+
return <Container something={true} />;
40142
}
41143
```
144+
145+
146+
The query object is also passed down as a prop, so, if you ever need it you can access it from there.
147+
148+
For a non-reactive query, we also pass `refetch` function as prop, which simply refetches the query from the database,
149+
and updates the components properly:
150+
151+
```jsx harmony
152+
import React from 'react';
153+
import {withQuery} from 'meteor/cultofcoders:grapher-react';
154+
155+
const PostList = ({data, isLoading, error, refetch}) => {
156+
return (
157+
<div>
158+
<a onClick={refetch}>Reload the data</a>
159+
{/* Rest of the component */}
160+
</div>
161+
)
162+
}
163+
164+
export default withQuery((props) => {
165+
return getPostLists.clone();
166+
}, {reactive: false})(PostList)
167+
```
168+
169+
If you container wraps a single object, and not a list of objects, you can configure your query like this:
170+
171+
```jsx harmony
172+
const UserProfile = ({data, isLoading, error}) => {
173+
return (
174+
<div>{data.email}</div>
175+
)
176+
};
177+
178+
export default withQuery((props) => {
179+
return getUserProfile.clone({userId: props.userId});
180+
}, {
181+
single: true
182+
})(UserProfile)
183+
```
184+
185+
You will find yourself repeating the same code over and over again for when the query isLoading or it errored. For this you can do:
186+
```jsx harmony
187+
function ErrorComponent({error}) {
188+
return <div>{error.reason}</div>
189+
};
190+
191+
function LoadingComponent() {
192+
return <div>Please wait...</div>
193+
};
194+
195+
const UserProfile = ({data}) => {
196+
return (
197+
<div>{data.email}</div>
198+
)
199+
}
200+
201+
export default withQuery((props) => {
202+
return getUserProfile.clone({userId: props.userId});
203+
}, {
204+
single: true,
205+
errorComponent: ErrorComponent,
206+
loadingComponent: LoadingComponent
207+
})(UserProfile)
208+
```
209+
210+
The `UserProfile` component will not render if it's loading or it errored.
211+
212+
To make things even more simple, you can globally define these rules, and all the components by default will have those options.
213+
214+
```jsx harmony
215+
import {setDefaults} from 'meteor/cultofcoders:grapher-react';
216+
217+
setDefaults({
218+
reactive: false, // you can default it to true
219+
single: false, // doesn't make sense to default this to true
220+
errorComponent: ErrorComponent,
221+
loadingComponent: LoadingComponent
222+
})
223+
```
224+
225+
If you need custom behavior for a specific component for `error` and `loading` components you can simply do:
226+
227+
```jsx harmony
228+
export default withQuery((props) => {
229+
return getUserProfile.clone({userId: props.userId});
230+
}, {
231+
errorComponent: null,
232+
loadingComponent: AnotherLoadingComponent,
233+
})(UserProfile)
234+
```

defaults.js

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export default {
2+
reactive: false,
3+
single: false,
4+
}

grapher-react.js

-3
This file was deleted.

lib/createQueryContainer.js legacy/createQueryContainer.js

+3-5
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ import React from 'react';
22
import {createContainer} from 'meteor/react-meteor-data';
33

44
export default (query, component, options = {}) => {
5-
(new SimpleSchema({
6-
reactive: {type: Boolean, defaultValue: false},
7-
single: {type: Boolean, defaultValue: false},
8-
dataProp: {type: String, defaultValue: 'data'}
9-
})).clean(options);
5+
if (Meteor.isDevelopment) {
6+
console.warn('createQueryContainer() is deprecated, please use withQuery() instead')
7+
}
108

119
if (options.reactive) {
1210
return createContainer((props) => {

lib/createReactiveContainer.js

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import {withTracker} from 'meteor/react-meteor-data';
2+
3+
/**
4+
* Wraps the query and provides reactive data fetching utility
5+
*
6+
* @param props
7+
* @param handler
8+
* @param config
9+
* @param queryContainer
10+
*/
11+
export default function createReactiveContainer(props, handler, config, queryContainer) {
12+
let subscriptionError;
13+
14+
return withTracker((props) => {
15+
const query = handler(props);
16+
17+
const subscriptionHandle = query.subscribe({
18+
onStop(err) {
19+
if (err) {
20+
subscriptionError = err;
21+
}
22+
},
23+
onReady() {
24+
subscriptionError = null;
25+
}
26+
});
27+
28+
const isReady = subscriptionHandle.ready();
29+
30+
return {
31+
grapher: {
32+
isLoading: !isReady,
33+
data: query.fetch(),
34+
error: subscriptionError,
35+
},
36+
config,
37+
props,
38+
}
39+
})(queryContainer)
40+
}

lib/createStaticContainer.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from 'react';
2+
3+
/**
4+
* Wraps the query and provides static data fetching utility
5+
*
6+
* @param props
7+
* @param handler
8+
* @param config
9+
* @param staticQueryContainer
10+
*/
11+
export default function createStaticContainer(props, handler, config, staticQueryContainer) {
12+
const query = handler(props);
13+
14+
return React.createElement(staticQueryContainer, {
15+
query,
16+
props,
17+
config
18+
})
19+
}

lib/getDisplayName.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function getDisplayName(WrappedComponent) {
2+
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
3+
}

0 commit comments

Comments
 (0)