I'm fairly new to React and even more new to flux and i'm having troubles finding info on Google about how to handle routing and flux.
I'm using the Meteor stack and the router (FlowRouter) has an imperative API for transitioning routes using FlowRouter.go('routeName, params).
Should I create a Link component that triggers an action and the action creator calls the this FlowRouter.go method?
Also it has a reactive API for the params so I can watch it and trigger an action if something changes (so stores can update).
An approach I took with a recent project while experimenting with Flux was to make the routing layer just another store. I made a Folder that contained React View Component File, Router Store and Router Actions. I am Providing The source Below:
React View File:
var React = require('react');
var storeMixin = require('project/shared/helpers/storeMixin');
var RouterStore = require('../RouterStore');
module.exports = React.createClass({
mixins: [storeMixin(RouterStore)],
getInitialState: function () {
return {RouterStore: RouterStore};
},
getComponentClass: function (route) {
switch (route) {
case 'help':
return require('project/app/components/Help');
default:
return require('project/FrontPage/FrontPage');
}
},
render: function () {
var props = {
route: this.state.RouterStore.get('route'),
routeParams: this.state.RouterStore.get('params')
};
var Component = this.getComponentClass(props.route);
return <Component {...props} />;
}
});
The Store File:
var Backbone = require('backbone');
var Store = require('project/shared/libs/Store');
var conf = require('./settings');
var constants = require('./constants');
class RouterModel extends Store.Model {
constructor() {
this.defaults = {
route: conf.ROUTE_DEFAULT,
params: []
};
super();
}
initialize() {
this._router = new AppRouter(this, conf.ROUTE_ROUTES);
}
handleDispatch(payload) {
switch (payload.actionType) {
case constants.ROUTE_NAVIGATE:
this._router.navigate(payload.fragment, {
trigger: payload.trigger,
replace: payload.replace
});
break;
}
}
}
class AppRouter extends Backbone.Router {
initialize(store, routes) {
this.store = store;
var route, key;
for (key in routes) {
if (routes.hasOwnProperty(key)) {
route = routes[key];
this.route(key, route, function(/* route, args... */) {
this.emitRouteAction.apply(this, arguments);
}.bind(this, route));
}
}
// catch all non-matching urls
Backbone.history.handlers.push({
route: /(.*)/,
callback: function() {
store.set({
route: constants.ROUTE_DEFAULT,
params: []
});
}
});
Backbone.$(document).on("ready", function() {
Backbone.history.start();
});
}
emitRouteAction(/* route, args... */) {
this.store.set({
route: arguments[0],
params: [].slice.call(arguments, 1)
});
}
}
module.exports = new RouterModel();
The Actions File:
var constants = require('./constants');
var dispatch = require('project/shared/helpers/dispatch');
var _ = require('underscore');
module.exports = {
navigate: function(fragment, trigger, replace) {
dispatch(constants.ROUTE_NAVIGATE, {
fragment: fragment,
trigger: _.isUndefined(trigger) ? true : trigger,
replace: _.isUndefined(replace) ? true : replace
});
}
};
Related
Im new with react and i have created a react project. I would like to know how i can use this default starter project to implement the code from:
code link from google. The code is as the following, credit to google, linked above.
Css file-
/* Always set the map height explicitly to define the size of the div
* element that contains the map. */
#map {
height: 100%;
}
/* Optional: Makes the sample page fill the window. */
html, body {
height: 100%;
margin: 0;
padding: 0;
}
HTML file-
<div id="map"></div>
<!-- Replace the value of the key parameter with your own API key. -->
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCkUOdZ5y7hMm0yrcCQoCvLwzdM6M8s5qk&libraries=places&callback=initMap" async defer></script>
Java script (pure js) file -
// This example requires the Places library. Include the libraries=places
// parameter when you first load the API. For example:
// <script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places">
var map;
var service;
var infowindow;
function initMap() {
var sydney = new google.maps.LatLng(-33.867, 151.195);
infowindow = new google.maps.InfoWindow();
map = new google.maps.Map(
document.getElementById('map'), {center: sydney, zoom: 15});
var request = {
query: 'Museum of Contemporary Art Australia',
fields: ['name', 'geometry'],
};
service = new google.maps.places.PlacesService(map);
service.findPlaceFromQuery(request, function(results, status) {
if (status === google.maps.places.PlacesServiceStatus.OK) {
for (var i = 0; i < results.length; i++) {
createMarker(results[i]);
}
map.setCenter(results[0].geometry.location);
}
});
}
function createMarker(place) {
var marker = new google.maps.Marker({
map: map,
position: place.geometry.location
});
google.maps.event.addListener(marker, 'click', function() {
infowindow.setContent(place.name);
infowindow.open(map, this);
});
}
I understand i need to create my key with google maps js and google places, however since Im new to react I'm unsure to how i could implement this into my new react project. Coudl some one show me how these files of code could be put together to be fit for a react project please. I apologies if i sound all over the place.
You can refer to this code I made. Remember to change the value of "YOUR_API_KEY" so that the map will work properly.
Here is the App.js code snippet:
import React from 'react';
import './App.css';
import Map from './components/placeSearch';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
map: {}
}
}
handleMapLoad = (map) => {
this.setState({
map: map
})
}
render() {
return (
<div className="App">
<Map id="myMap" options={{center: { lat: 51.501904, lng: -0.115871 }, zoom: 13}} onMapLoad = {this.handleMapLoad}/>
</div>
);
}
}
export default App;
The code for Place Search can be found in the Map components in placeSearch.js. Change the value of your API key here.
import React from "react";
import ReactDOM from 'react-dom';
const map;
var markers = [];
var infowindow;
const API_KEY = "YOUR_API_KEY";
var place = [];
class Map extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
const script = document.createElement('script');
script.type = 'text/javascript';
script.src = `https://maps.googleapis.com/maps/api/js?key=` + API_KEY + `&libraries=geometry,places`;
script.id = 'googleMaps';
script.async = true;
script.defer = true;
document.body.appendChild(script);
script.addEventListener('load', e => {
this.onScriptLoad()
})
}
onScriptLoad() {
map = new window.google.maps.Map(document.getElementById(this.props.id), this.props.options);
this.props.onMapLoad(map)
var request = {
query: 'Museum of Contemporary Art Australia',
fields: ['name', 'geometry'],
};
var service = new google.maps.places.PlacesService(map);
service.findPlaceFromQuery(request, function(results, status) {
if (status === google.maps.places.PlacesServiceStatus.OK) {
for (var i = 0; i < results.length; i++) {
var place = results[i];
var marker = new google.maps.Marker({
map: map,
position:place.geometry.location,
title: place.formatted_address,
});
markers.push(marker);
infowindow = new google.maps.InfoWindow();
marker.addListener('click', () => {
infowindow.setContent(place.name);
infowindow.open(map, marker);
});
}
map.setCenter(results[0].geometry.location);
}
})
}
render() {
return (
<div id = "root" >
<div className = "map" id = {this.props.id}/>
</div>
)
}
}
export default Map;
Hope this helps!
I'm trying to test if my MyCustomMap is calling my MapController and creating a new leaflet map component.
However MapController is setting _mapGroup as undefined on MyCustomMap component and it should be L.layerGroup() (not undefined).
I already tried to mock MapController and leaflet in different ways, but _mapGroup still being undefined. What is wrong? How can I solve this?
My MyCustomMap test file (custom-map.test.js):
import MyCustomMap from './custom-map';
import initialMapOptions from './map-options';
import MapController from './map-controller';
const mapCreateGroupMock = jest.fn(),
mapAddGroupToMap = jest.fn();
let myMap = null,
mapController = null;
beforeAll(() => {
const mapOptions = initialMapOptions();
mapController = {
createGroup: mapCreateGroupMock,
addGroupToMap: mapAddGroupToMap
};
myMap = new MyCustomMap(mapController, mapOptions);
});
describe('My custom map', () => {
it(`should call MapController and create a new leaflet map component`, () => {
expect(mapCreateGroupMock).toBeCalledTimes(1);
expect(mapAddGroupToMap).toBeCalledTimes(1);
expect(myMap._mapGroup).not.toBeNull(); // -> here is failing because myMap._mapGroup is undefined and shouldn't be
});
});
My MapController (map-controller.js):
import L from 'leaflet';
class MapController {
constructor(container, configuration) {
this._map = new L.Map(container, { zoomControl: false, minZoom: this._minZoom });
this._container = document.getElementById(container);
//more code here ...
}
createGroup() {
return L.layerGroup();
}
addGroupToMap(group) {
this._map.addLayer(group);
}
//more code here ...
}
export default MapController;
My MyCustomMap component (custom-map.js):
class MyCustomMap {
constructor(mapController, configuration) {
this._mapController = mapController;
this._configuration = configuration;
this._mapGroup = this._mapController.createGroup();
this._mapController.addGroupToMap(this._mapGroup);
//more code here ...
}
}
Solved! My mapCreateGroupMock was simply returning an empty function and I needed to simulate a return value. So I:
mocked that value;
perform a mockClear() on my mock functions;
did every that steps on beforeEach();
and changed my myMap._mapGroup assert to .toBeTruthy() to check if is not undefined and not null.
Now it work as expected.
Final changes on MyCustomMap test file (custom-map.test.js):
import MyCustomMap from './custom-map';
import initialMapOptions from './map-options';
const mapCreateGroupMock = jest.fn(),
mapAddGroupToMap = jest.fn(),
mapOptions = initialMapOptions();
let myMap = null,
mapController = null;
describe('My custom map', () => {
beforeEach(() => {
mapCreateGroupMock.mockClear();
mapAddGroupToMap.mockClear();
const addLayerMock = jest.fn(),
mapGroup = {
addLayer: addLayerMock
};
mapCreateGroupMock.mockReturnValueOnce(mapGroup);
mapController = {
createGroup: mapCreateGroupMock,
addGroupToMap: mapAddGroupToMap
};
myMap = new MyCustomMap(mapController, mapOptions);
});
it(`should call MapController and create a new leaflet map component`, () => {
expect(mapCreateGroupMock).toBeCalledTimes(1);
expect(mapAddGroupToMap).toBeCalledTimes(1);
expect(myMap._mapGroup).toBeTruthy();
});
});
I just have two components, and the app is connected to firebase (firestore). The data called in ngOnInit disappears when I navigate and only appears again if I insert a Post or a User (forms).Why? Can someone help me out?
The routes:
const routes: Routes = [
{ path: "", component: HomeComponent, pathMatch: "full" },
{
path: "admin",
component: AdminComponent
},
{
path: "**",
redirectTo: "/",
pathMatch: "full"
}
];
The Service
itemsCollection: AngularFirestoreCollection<Post>;
items: Observable<Post[]>;
itemDoc: AngularFirestoreDocument<Post>;
constructor(public afs: AngularFirestore) {
this.itemsCollection = this.afs.collection('posts', ref =>
ref.orderBy('date', 'desc')
);
this.items = this.itemsCollection.snapshotChanges().map(changes => {
return changes.map(a => {
const data = a.payload.doc.data() as Post;
data.id = a.payload.doc.id;
return data;
});
});
}
getItems() {
return this.items;
}
Its Component and the subscription
private postService: PostsService,
) {}
ngOnInit() {
this.postService.getItems().subscribe(data => {
this.postInput = data;
});
}
Create posts property in your service.
Then subscribe to your observable in your service, set posts to what u get from firebase.
Also dont write your code inside constructor but wrap in in a function and use it in OnInit in apropriate component. And lastly create a Subject send with it the current posts and subscribe to them in onInit inside desired components
posts = Post[]
postsChanged = new Subject<Post[]>()
getItems(){
this.itemsCollection.snapshotChanges().map(changes => {
return changes.map(a => {
return {
id: a.payload.doc.id,
...a.payload.doc.data()
}
}).subscribe(posts => {
this.posts = posts
this.postsChanged.next([...this.posts])
})
}
Based on Kase44 hints i managed to find a solution: wrapped the capture of the ID in a function, called it in the service constructor and in the ngOnInit():
The Service
export class UsersService {
usersCollection: AngularFirestoreCollection<Users>;
userDoc: AngularFirestoreDocument<Users>;
users: Observable<Users[]>;
user: Observable<Users>;
constructor(public afs: AngularFirestore) {
this.usersCollection = this.afs.collection('employees', ref =>
ref.orderBy('name', 'asc')
);
this.getUserID();
}
getUserID(){
this.users = this.usersCollection.snapshotChanges().map(changes => {
return changes.map(a => {
const data = a.payload.doc.data() as Users;
data.id = a.payload.doc.id;
return data;
});
});
}
And the component:
ngOnInit() {
this.userService.getUsers().subscribe(data => {
this.emp = data;
});
this.userService.getUserID();
}
I'm getting the following error even though I'm pretty sure I have specified the URL in my collection:
Uncaught Error: A `url property or function must be specified
Here's my collection:
import $ from 'jquery';
import Backbone from 'backbone';
import ViewModel from './view-model';
import InstancesCollection from '../instances/instances-collection-model';
export default Backbone.Collection.extend({
model: ViewModel,
url: $.createURL('/api/view'),
initialize: (models = []) => {
models.forEach(element => {
element.instances = new InstancesCollection(element.instances);
});
},
parse: (response) => {
return response.views;
}
});
I tried to follow the response given here but in vain. Does anybody know if I'm missing something here?
Just in case you needed to see the model as well, here it is:
import _ from 'lodash';
import Backbone from 'backbone';
import InstancesCollection from '../instances/instances-collection-model';
export default Backbone.Model.extend({
defaults: {
id: null,
name: null,
count: 0,
instances: []
},
initialize: function (models) {
const attributes = Object.assign({}, this.defaults, models);
if (_.isArray(attributes.instances)) {
this.set({instances: new InstancesCollection(attributes.instances)}, {silent: true});
}
},
toJSON: function () {
let json = Object.assign({}, this.attributes);
for (const attr of Object.keys(json)) {
if ((json[attr] instanceof Backbone.Model) || (json[attr] instanceof Backbone.Collection)) {
json[attr] = json[attr].toJSON();
}
}
return json;
}
});
Any help would be appreciated. Thank you.
Is there an established pattern used to manage user interactions with individual components, such as displaying loader spinners, disabling input fields while a form is saving/loading, etc.?
I'm finding myself doing the following in my stores in order to keep components somewhat decoupled from any implied state:
function CampaignStore() {
EventEmitter.call(this);
AppDispatcher.register(payload => {
switch (payload.type) {
// [#1] ---------------v (main action)
case CampaignContants.SAVE:
// [#2] ------------------------v (prepare for the main action)
this.emit(CampaignContants.WILL_SAVE);
const data = payload.data;
if (data.id) {
// [#3] ---v (perform main action in store)
updateCampaign(payload.data).then(_ => {
// [#4] ------------------------v (after main action)
this.emit(CampaignContants.DID_SAVE, 0)
});
} else {
insertCampaign(payload.data).then(campaignId => this.emit(CampaignContants.DID_SAVE, campaignId));
}
break;
// ...
}
}
}
Basically, I just fire an event saying that some action is about to take place, then I perform the action (make API call, etc.), then emit another event when the action completes.
Inside a component, I can just subscribe to a WILL_<action> event, render all the spinners and such, then clear up the screen when the DID_<action> is fired. While this seems to work, it does feel pretty boilerplattie and repetitive, as well as super messy (way too much state that only exists to tweak the UI based on where an action is (between WILL_<action> and *DID_<action>.
// some component
var MyComponent = React.createClass({
getInitialState: function () {
return {
items: [],
loading: false,
saving: false,
checkingPasswordStrength: fase,
// ...
};
},
render: function(){
return (
<div>
{this.state.loading && (
<p>Loading...</p>
)}
{!this.state.loading && (
// Display component in not-loading state
)}
</div>
);
}
});
I think you would be better off using the lifecycle methods such as componentWillMount, componentDidMount, componentWillUpdate, and componentWillUnmount. Using those methods you can inspect the previous/current/next props/state (depending on the method) and respond to that. That way your store only handles your state, and your components become more pure.
we have found a simple loading container component helps here.
so something like this:
const LoadingContainer = React.createClass({
getDefaultProps: function() {
return {isLoadedCheck:(res) => res.data!=null }
},
getInitialState: function() {
return {isLoaded:false, errors:[]}
},
componentDidMount: function() {
if(this.props.initialLoad) { this.props.initialLoad(); }
if(this.props.changeListener) { this.props.changeListener(this.onChange); }
},
onChange: function() {
let res = this.props.loadData();
this.setState({errors: res.errors, isLoaded: this.props.isLoadedCheck(res)});
},
render: function() {
if(!this.state.isLoaded) {
let errors = this.state.errors && (<div>{this.state.errors.length} errors</div>)
return (<div>{errors}<LoadingGraphic /> </div>)
}
return <div>{this.props.children}</div>
}
});
const Wrapper = React.createClass({
getDefaultProps: function() {
return {id:23}
},
render: function() {
let initialLoad = () => someActionCreator.getData(this.props.id);
let loadData = () => someStore.getData(this.props.id);
let changeListener = (fn) => someStore.onChange(fn);
return (<div><LoadingContainer initialLoad={initialLoad}
changeListener={changeListener}
loadData={loadData}
isLoadedCheck={(res) => res.someData != null}><SomeComponent id={this.props.id} /></LoadingContainer></div>)
}
});
while it adds another stateless wrapper, it gives a clean way to make sure your components dont just load on mount and a common place to show api feedback etc.
and with react 14 these sort of pure stateless wrappers are getting a bit of a push, with perf improvements to come, so we've found it scales nicely
This is the pattern which will help you in getting your individual components to manage user interactions
var MyComponent = React.createClass({
getInitialState: function() {
return {
item: [],
loading: true,
};
},
componentDidMount: function() {
//Make your API calls here
var self = this;
$.ajax({
method: 'GET',
url: 'http://jsonplaceholder.typicode.com/posts/1',
success: function(data) {
if (self.isMounted()) {
self.setState({
item: data,
loading: false
});
}
}
});
},
render: function() {
var componentContent = null;
if (this.state.loading) {
componentContent = (<div className="loader"></div>);
} else {
componentContent = (
<div>
<h4>{this.state.item.title}</h4>
<p>{this.state.item.body}</p>
</div>
);
}
return componentContent;
}});