1
+ import "package:react/react.dart" as react;
2
+ import "package:react/react_client.dart" ;
3
+ import "dart:html" ;
4
+ import "dart:convert" ;
5
+ import "dart:async" ;
6
+
7
+ /*
8
+ * Hello,
9
+ *
10
+ * this is a part of the tutorial to the react-dart package. We'll go through
11
+ * a simple app that is quering Google Maps API and showing the result to the
12
+ * user. It also stores the search history and allows to reload past queries.
13
+ *
14
+ * In this file you'll find the structure and the logic of the app.
15
+ * There is also a `geocodes.html` file, that contains the mountpoint.
16
+ *
17
+ * Be sure that you understand the basic concepts of
18
+ * [React](http://facebook.github.io/react/) before reading this tutorial.
19
+ *
20
+ * Enjoy!
21
+ */
22
+
23
+
24
+ /*
25
+ * Divide your app to the components and conquer!
26
+ * There is a first custom component. It is just a table row showing
27
+ * one item of the response to the user.
28
+ */
29
+ class _GeocodesResultItem extends react.Component {
30
+
31
+ /*
32
+ * The only function you must implement in the custom component is `render`.
33
+ * It just returns the the structure of the other components.
34
+ *
35
+ * Every component has a map of properties called `props`. It can be
36
+ * specified during creation.
37
+ */
38
+ render () {
39
+ return react.tr ({}, [
40
+ react.td ({}, props['lat' ]),
41
+ react.td ({}, props['lng' ]),
42
+ react.td ({}, props['formatted' ])
43
+ ]);
44
+ }
45
+ }
46
+
47
+ /*
48
+ * Now we need to tell React that there exists our custom component.
49
+ * As a reward, it gives us a function, that takes the properties
50
+ * and returns our element. You'll see it in action shortly.
51
+ * It is the only correct way to create your component. Do not use the
52
+ * constructor!
53
+ */
54
+ var geocodesResultItem =
55
+ react.registerComponent (() => new _GeocodesResultItem ());
56
+
57
+
58
+ /*
59
+ * In this component we'll
60
+ */
61
+ class _GeocodesResultList extends react.Component {
62
+
63
+ render () {
64
+ /*
65
+ * Built-in components have also properties.
66
+ * They correspond to the html props.
67
+ */
68
+ return react.div ({'id' : 'results' }, [
69
+ react.h2 ({}, "Results:" ),
70
+ /*
71
+ * However, `class` is a keyword in javascript, therefore
72
+ * `className` is used instead
73
+ */
74
+ react.table ({'className' : 'table' }, [
75
+ react.thead ({}, [
76
+ react.th ({}, 'Latitude' ),
77
+ react.th ({}, 'Longitude' ),
78
+ react.th ({}, 'Address' )
79
+ ]),
80
+ react.tbody ({},
81
+ /*
82
+ * The second argument contains the body of the component
83
+ * (as you have already seen). It can be a string,
84
+ * a component or an iterable.
85
+ */
86
+ props['data' ].map (
87
+ // Usecase for our custom component.
88
+ (item) => geocodesResultItem ({
89
+ 'lat' : item['geometry' ]['location' ]['lat' ],
90
+ 'lng' : item['geometry' ]['location' ]['lng' ],
91
+ 'formatted' : item['formatted_address' ]
92
+ })
93
+ )
94
+ )
95
+ ])
96
+ ]);
97
+ }
98
+ }
99
+
100
+ var geocodesResultList =
101
+ react.registerComponent (() => new _GeocodesResultList ());
102
+
103
+
104
+ /*
105
+ * On the search form is ilustrated that:
106
+ * - the functions can be component parameters (handy for callbacks)
107
+ * - the DOM elements can accessed using refs.
108
+ */
109
+ class _GeocodesForm extends react.Component {
110
+
111
+ render () {
112
+ return react.div ({}, [
113
+ react.h2 ({}, "Search" ),
114
+ // Component function is passed as callback
115
+ react.form ({'onSubmit' : onFormSubmit}, [
116
+ react.input ({
117
+ 'type' : 'text' ,
118
+ 'placeholder' : 'Enter address' ,
119
+ // Input is referenced to access it's value
120
+ 'ref' : 'addressInput'
121
+ }),
122
+ react.input ({'type' : 'submit' }),
123
+ ])
124
+ ]);
125
+ }
126
+
127
+ // This is called when form is submited
128
+ onFormSubmit (e) {
129
+ e.preventDefault ();
130
+ // The input's value is accessed.
131
+ var addr = ref ('addressInput' ).value;
132
+ ref ('addressInput' ).value = "" ;
133
+ // And the callback from the parent element is called.
134
+ // (Yes, you haven't seen it yet.)
135
+ props['submiter' ](addr);
136
+ }
137
+ }
138
+
139
+ var geocodesForm = react.registerComponent (() => new _GeocodesForm ());
140
+
141
+ /*
142
+ * Nothing new here. Item in history list.
143
+ */
144
+ class _GeocodesHistoryItem extends react.Component {
145
+
146
+ reload (e) {
147
+ props['reloader' ](props['query' ]);
148
+ }
149
+
150
+ render () {
151
+ return react.li ({}, [
152
+ react.button ({'onClick' : reload}, 'Reload' ),
153
+ " (${props ['status' ]}) ${props ['query' ]}"
154
+ ]);
155
+ }
156
+ }
157
+
158
+ var geocodesHistoryItem =
159
+ react.registerComponent (() => new _GeocodesHistoryItem ());
160
+
161
+
162
+ /*
163
+ * And the whole history list. Note, that it just
164
+ * passes the callback from the parent.
165
+ */
166
+ class _GeocodesHistoryList extends react.Component {
167
+
168
+ render () {
169
+ return react.div ({}, [
170
+ react.h3 ({}, "History:" ),
171
+ react.ul ({},
172
+ new List .from (props['data' ].keys.map (
173
+ (key) => geocodesHistoryItem ({
174
+ 'key' : key,
175
+ 'query' : props['data' ][key]["query" ],
176
+ 'status' : props['data' ][key]["status" ],
177
+ 'reloader' : props['reloader' ]
178
+ })
179
+ )).reversed
180
+ )
181
+ ]);
182
+ }
183
+ }
184
+
185
+ var geocodesHistoryList =
186
+ react.registerComponent (() => new _GeocodesHistoryList ());
187
+
188
+
189
+ /*
190
+ * The core component of the App.
191
+ *
192
+ * Introduces the state. State and the properties are the two places to store
193
+ * the component's data. However they differ in the use:
194
+ * - the properties contain data dictated by the parent component
195
+ * - the state is an internal storage of the component that can't
196
+ * be accessed by the parent. When the state is changed,
197
+ * the whole component is repainted.
198
+ *
199
+ * It's a common practice to store the aplication data in the state of the
200
+ * root component. It will redpaint every time, the state is changed. However,
201
+ * it is not required - you can use normal variables and repaint manualy.
202
+ *
203
+ * When the request is sent, it has `pending` status in the history.
204
+ * This changes to `OK` or `error` when the answer (or timeout) comes.
205
+ * If the new request is sent meanwhile, the old one is `canceled`.
206
+ */
207
+ class _GeocodesApp extends react.Component {
208
+
209
+ getInitialState () => {
210
+ 'shown_addresses' : [], // Data from last query.
211
+ 'history' : {} // Map of past queries.
212
+ };
213
+
214
+ var last_id = 0 ; // The id of the last query.
215
+
216
+ /*
217
+ * Sends the query to the API and processes the result
218
+ */
219
+ newQuery (String addr) {
220
+
221
+ /*
222
+ * Once the query is being sent, it appears in the history
223
+ * and is given an id.
224
+ */
225
+ var id = addQueryToHistory (addr);
226
+
227
+ // Prepare the URL
228
+ addr = Uri .encodeQueryComponent (addr);
229
+ var path =
230
+ 'https://maps.googleapis.com/maps/api/geocode/json?address=$addr ' ;
231
+
232
+ // Send the request
233
+ HttpRequest .getString (path)
234
+ .then ((value) =>
235
+ // Delay the answer 2 more seconds, for the test purposes
236
+ new Future .delayed (new Duration (seconds: 2 ), ()=> value)
237
+ )
238
+ .then ((String raw) {
239
+ // Is this the answer to the last request?
240
+ if (id == last_id){
241
+ // If yes, query was `OK` and `shown_adresses` are replaced
242
+ state['history' ][id]['status' ]= 'OK' ;
243
+ var data = JSON .decode (raw);
244
+ /*
245
+ * Calling `setState` will update the state and then
246
+ * repaint the component.
247
+ *
248
+ * In theory, state should be considered as immutable
249
+ * and `setState` or `replaceState` should be the only way
250
+ * to change it.
251
+ *
252
+ * It is possible to do this, when the whole state value is parsed
253
+ * from the server response (the case of `shown_addresses`).
254
+ * However, it would be inefficient to copy whole `history` just to
255
+ * change one item. Therefore we mutate it and then
256
+ * replace it by itself.
257
+ *
258
+ * Have a look at vacuum_persistent package to achieve
259
+ * the immutability of state.
260
+ */
261
+ setState ({
262
+ 'shown_addresses' : data['results' ],
263
+ 'history' : state['history' ]
264
+ });
265
+ } else {
266
+ // Otherwise, query was `canceled`
267
+ state['history' ][id]['status' ]= 'canceled' ;
268
+ setState ({'history' : state['history' ]});
269
+ }
270
+ })
271
+ .catchError ((Error error) {
272
+ state['history' ][id]['status' ]= 'error' ;
273
+ setState ({'history' : state['history' ]});
274
+ });
275
+ }
276
+
277
+ /*
278
+ * Add a new query to the history with the `pending` state,
279
+ * and return it's id.
280
+ */
281
+ addQueryToHistory (String query) {
282
+ var id = ++ last_id;
283
+ state['history' ][id] = {
284
+ "query" : query,
285
+ "status" : "pending"
286
+ };
287
+ setState ({'history' : state['history' ]});
288
+ return id;
289
+ }
290
+
291
+ render () {
292
+ return react.div ({}, [
293
+ react.h1 ({}, "Geocode resolver" ),
294
+ geocodesResultList ({
295
+ // The state values are passed to the children as the properties.
296
+ 'data' : state['shown_addresses' ]
297
+ }),
298
+ geocodesForm ({
299
+ // `newQuery` is the final callback of the button presses.
300
+ 'submiter' : newQuery
301
+ }),
302
+ geocodesHistoryList ({
303
+ 'data' : state['history' ],
304
+ // The same here.
305
+ 'reloader' : newQuery
306
+ })
307
+ ]);
308
+ }
309
+ }
310
+
311
+ var geocodesApp = react.registerComponent (() => new _GeocodesApp ());
312
+
313
+ /*
314
+ * And, finally, few magic commands to make it work!
315
+ *
316
+ * Select the root of the app and the place, where it lives.
317
+ */
318
+ void main () {
319
+ setClientConfiguration ();
320
+ react.render (geocodesApp ({}), querySelector ('#content' ));
321
+ }
0 commit comments