I am building an application with pure javascript and Web Components. I also want to use the MVC Pattern, but now I have a problem with asynchronous calls from the model.
I am developing a meal-list component. The data is coming from an API as JSON in the following format:
[
{
id: 1,
name: "Burger",
},
]
I want the controller to get the data from the model and send it to the view.
meals.js (Model)
export default {
get all() {
const url = 'http://localhost:8080/meals';
let speisekarte = [];
fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}).then(res => {
return res.json()
}).then(data => {
// This prints the result I want to use, but I can't return
console.log(data);
// This does not work
speisekarte = data;
// This also does not work
return data;
});
// is undefined.
return speisekarte;
},
}
This is how I tried to get the data from an API.
meal-list.component.js (Controller)
import Template from './meal-list.template.js'
import Meal from '../../../../data/meal.js'
export default class MealListComponent extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
// Should send the Data from the model to the View
this.shadowRoot.innerHTML = Template.render(Meal.all);
}
}
if (!customElements.get('mp-meal-list')) {
customElements.define('mp-meal-list', MealListComponent);
}
meal-list.template.js (View)
export default {
render(meals) {
return `${this.html(meals)}`;
},
html(meals) {
let content = `<h1>Speisekarte</h1>
<div class="container">`;
content += /* display the data from api with meals.forEach */
return content + '</div>';
},
}
As I mentioned in the comments, I have a problem in returning the async data from the model to the view. Either it is undefined when I try to return data; or if I try to save the data into an array. I could also return the whole fetch() method, but this returns a promise and I dont think the controller should handle the promise.
I already read the long thread in How do I return the response from an asynchronous call? but I could not relate it to my case.
Since you declared speisekarte as an array, I'd expect it to always return as an empty array. When the fetch executes and fulfills the promise, its always too late in the above implementation.
You have to wait for the fetch result and there are multiple options you might consider:
Either providing a callback to the fetch result
Or notifying your application via event dispatch and listeners that your data has been loaded, so it can start rendering
Your link already has a very good answer on the topic callbacks and async/await, I could not put it better than what is explained there.
Thanks to lotype and Danny '365CSI' Engelman I've found the perfect solution for my projct. I solved it with custom events and an EventBus:
meal.js (model)
get meals() {
const url = 'http://localhost:8080/meals';
return fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}).then(res => {
return res.json()
}).then(data => {
let ce = new CustomEvent(this.ESSEN_CHANGE_EVENT, {
detail: {
action: this.ESSEN_LOAD_ACTION,
meals: data,
}
});
EventBus.dispatchEvent(ce);
});
},
EventBus.js (from book: Web Components in Action)
export default {
/**
* add event listener
* #param type
* #param cb
* #returns {{type: *, callback: *}}
*/
addEventListener(type, cb) {
if (!this._listeners) {
this._listeners = [];
}
let listener = {type: type, callback: cb};
this._listeners.push(listener);
return listener;
},
/**
* trigger event
* #param ce
*/
dispatchEvent(ce) {
this._listeners.forEach(function (l) {
if (ce.type === l.type) {
l.callback.apply(this, [ce]);
}
});
}
}
Now, when the data is ready, a signal to the event bus is sent. The meal-list-component is waiting for the events and then gets the data:
export default class MealListComponent extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = Template.render();
this.dom = Template.mapDOM(this.shadowRoot);
// Load Speisekarte on init
this.dom.meals.innerHTML = Template.renderMeals(MealData.all);
// Custom Eventlistener - always triggers when essen gets added, deleted, updated etc.
EventBus.addEventListener(EssenData.ESSEN_CHANGE_EVENT, e => {
this.onMealChange(e);
});
}
onMealChange(e) {
switch (e.detail.action) {
case EssenData.ESSEN_LOAD_ACTION:
this.dom.meals.innerHTML = Template.renderMEals(e.detail.meals);
break;
}
}
}
Related
I have some component with a table which has actions buttons. When clicking on the button, the component emits an action, for example: (edit, delete,route)
getEvent(action: ActionButtons, object: any) {
// type: (edit,delete,route), route: info where to redirect
const {type, route} = action;
this.action.emit({ type, route, body: object });
}
In the parent component I catch this object by the following function and do some logic depending on the action:
getAction({type, route, body: {...faculty }}) {
const action = {
edit: () => {
this.openFacultyModal(faculty);
},
delete: () => {
this.openConfirmDialog(faculty);
},
route: () => {
this.redirecTo(faculty.faculty_id);
}
};
action[type]();
}
The poblem is, if I want to use the table in another component I have to cut and paste getAction() and just change the function inside object.
It turns out that there will be code duplication.
Is it possible to somehow solve the problem of code duplication using closures or creating a separate class?
You can make your action map object reusable:
const actions = {
edit(target, faculty) {
target.openFacultyModal(faculty);
},
delete(target, faculty) {
target.openConfirmDialog(faculty);
},
route(target, faculty) {
target.redirecTo(faculty.faculty_id);
}
};
Then use it where necessary, for instance in getAction:
getAction({type, route, body: {...faculty }}) {
actions[type](this, faculty);
}
As the tittle says, I would like to be able to use the same axios response for differents components.
I have some restrictions like, I'm onlyl able to use react by adding scripts tags to my html so things like exports or jsx are impossible for me.
This is my react code:
class User extends React.Component {
state = {
user: {}
}
componentWillMount() {
console.log(localStorage.getItem("user"))
axios.get('http://localhost:8080/dashboard?user=' + localStorage.getItem("user"))
.then(res => {
const userResponse = res.data
setTimeout(() =>
this.setState({user: userResponse.user}), 1000);
})
}
render () {
const {user} = this.state
if (user.fullName === undefined)
return React.createElement("div", null, 'loading..');
return React.createElement("span", {className: "mr-2 d-none d-lg-inline text-gray-600 small" }, user.fullName);
}
}
ReactDOM.render( React.createElement(User, {}, null), document.getElementById('userDropdown') );
class Roles extends React.Component{
state = {
user: {}
}
componentWillMount() {
console.log(localStorage.getItem("user"))
axios.get('http://localhost:8080/dashboard?user=' + localStorage.getItem("user"))
.then(res => {
const userResponse = res.data
setTimeout(() =>
this.setState({user: userResponse.user}), 1000);
})
}
render () {
const {user} = this.state
const roles = user.user.roles.map((rol) => rol.roleName)
if (user.fullName === undefined)
return React.createElement("div", null, 'loading..');
return React.createElement("a", {className: "dropdown-item" }, user.fullName);
}
}
ReactDOM.render( React.createElement(Roles, {}, null), document.getElementById('dropdownRol') );
I would like to be able to manage different components(rendering each one) with data of the same axios response.
Is this possible considering my limitations?
Thanks in advance
Here's a working example of how you might do it. I've tried to annotate everything with comments, but I'm happy to try to clarify if you have questions.
// Fake response object for the store's "load" request
const fakeResponse = {
user: {
fullName: "Carolina Ponce",
roles: [
{ roleName: "administrator" },
{ roleName: "editor" },
{ roleName: "moderator" },
{ roleName: "generally awesome person" }
]
}
};
// this class is responsible for loading the data
// and making it available to other components.
// we'll create a singleton for this example, but
// it might make sense to have more than one instance
// for other use cases.
class UserStore {
constructor() {
// kick off the data load upon instantiation
this.load();
}
// statically available singleton instance.
// not accessed outside the UserStore class itself
static instance = new this();
// UserStore.connect creates a higher-order component
// that provides a 'store' prop and automatically updates
// the connected component when the store changes. in this
// example the only change occurs when the data loads, but
// it could be extended for other uses.
static connect = function(Component) {
// get the UserStore instance to pass as a prop
const store = this.instance;
// return a new higher-order component that wraps the connected one.
return class Connected extends React.Component {
// when the store changes just force a re-render of the component
onStoreChange = () => this.forceUpdate();
// listen for store changes on mount
componentWillMount = () => store.listen(this.onStoreChange);
// stop listening for store changes when we unmount
componentWillUnmount = () => store.unlisten(this.onStoreChange);
render() {
// render the connected component with an additional 'store' prop
return React.createElement(Component, { store });
}
};
};
// The following listen, unlisten, and onChange methods would
// normally be achieved by having UserStore extend EventEmitter
// instead of re-inventing it, but I wasn't sure whether EventEmitter
// would be available to you given your build restrictions.
// Adds a listener function to be invoked when the store changes.
// Called by componentWillMount for connected components so they
// get updated when data loads, etc.
// The store just keeps a simple array of listener functions. This
// method creates the array if it doesn't already exist, and
// adds the new function (fn) to the array.
listen = fn => (this.listeners = [...(this.listeners || []), fn]);
// Remove a listener; the inverse of listen.
// Invoked by componentWillUnmount to disconnect from the store and
// stop receiving change notifications. We don't want to attempt to
// update unmounted components.
unlisten = fn => {
// get this.listeners
const { listeners = [] } = this;
// delete the specified function from the array.
// array.splice modifies the original array so we don't
// need to reassign it to this.listeners or anything.
listeners.splice(listeners.indexOf(fn), 1);
};
// Invoke all the listener functions when the store changes.
// (onChange is invoked by the load method below)
onChange = () => (this.listeners || []).forEach(fn => fn());
// do whatever data loading you need to do here, then
// invoke this.onChange to update connected components.
async load() {
// the loading and loaded fields aren't used by the connected
// components in this example. just including them as food
// for thought. components could rely on these explicit fields
// for store status instead of pivoting on the presence of the
// data.user object, which is what the User and Role components
// are doing (below) in this example.
this.loaded = false;
this.loading = true;
try {
// faking the data request. wait two seconds and return our
// hard-coded data from above.
// (Replace this with your network fetch.)
this.data = await new Promise(fulfill =>
setTimeout(() => fulfill(fakeResponse), 2000)
);
// update the loading/loaded status fields
this.loaded = true;
this.loading = false;
// call onChange to trigger component updates.
this.onChange();
} catch (e) {
// If something blows up during the network request,
// make the error available to connected components
// as store.error so they can display an error message
// or a retry button or whatever.
this.error = e;
}
}
}
// With all the loading logic in the store, we can
// use a much simpler function component to render
// the user's name.
// (This component gets connected to the store in the
// React.createElement call below.)
function User({ store }) {
const { data: { user } = {} } = store || {};
return React.createElement(
"span",
{ className: "mr-2 d-none d-lg-inline text-gray-600 small" },
user ? user.fullName : "loading (User)…"
);
}
ReactDOM.render(
// Connect the User component to the store via UserStore.connect(User)
React.createElement(UserStore.connect(User), {}, null),
document.getElementById("userDropdown")
);
// Again, with all the data loading in the store, we can
// use a much simpler functional component to render the
// roles. (You may still need a class if you need it to do
// other stuff, but this is all we need for this example.)
function Roles({ store }) {
// get the info from the store prop
const { data: { user } = {}, loaded, loading, error } = store || {};
// handle store errors
if (error) {
return React.createElement("div", null, "oh noes!");
}
// store not loaded yet?
if (!loaded || loading) {
return React.createElement("div", null, "loading (Roles)…");
}
// if we made it this far, we have user data. do your thing.
const roles = user.roles.map(rol => rol.roleName);
return React.createElement(
"a",
{ className: "dropdown-item" },
roles.join(", ")
);
}
ReactDOM.render(
// connect the Roles component to the store like before
React.createElement(UserStore.connect(Roles), {}, null),
document.getElementById("dropdownRol")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="userDropdown"></div>
<div id="dropdownRol"></div>
Has anyone implemented sending and receiving signals with opentok-react https://github.com/aiham/opentok-react? I can't find even a simple example on how to do it in React using opentok-react.
Thanks for using opentok-react. Unfortunately an easy way to do signaling hasn't yet been added to opentok-react so the following process is a bit convoluted.
To do signaling you will need to get access to the Session object and call the signal method on it as you normally would (See https://tokbox.com/developer/sdks/js/reference/Session.html#signal).
If you used the OTSession component you can access the Session object by getting a reference to the OTSession element (See https://reactjs.org/docs/refs-and-the-dom.html).
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.otSession = React.createRef();
}
render() {
return <OTSession ref={this.otSession} />;
}
}
and using its sessionHelper property to call the signal method:
this.otSession.current.sessionHelper.session.signal(...);
If you want to specify a particular target connection for the recipient then you need to get it from the underlying Publisher or Subscriber object's stream property. First get a reference to the OTPublisher or OTSubscriber element :
<OTPublisher ref={this.otPublisher} />
// or
<OTSubscriber ref={this.otSubscriber} />
Then get access to the Connection object:
this.otPublisher.current.getPublisher().stream.connection
// or
this.otSubscriber.current.getSubscriber().stream.connection
I have not tested this but once you have access to the Session and Connection objects then you can use the full signaling capabilities of the OpenTok JS SDK.
I Created a npm package 'opentok-rvc' with reference to opentok-react.
Here i created a listener to watch signaling and if any signal gets i send the signal to another event
// signal message listener inside npm package
session.on('signal:msg', function signalCallback(event) {
console.log(event);
onSignalRecieve(event);
});
Inside your component, please do the following
// to send the opentok signal
// here param data can be object for eg:
// data = { type: 'START_VIDEO_CALL', data: 'By Alex' }
onSignalSend = (data) => {
if (this.otSession.current !== null) {
this.otSession.current.sessionHelper.session.signal({
type: 'msg',
data: data
}, function signalCallback(error) {
if (error) {
console.log('onSignalSend Error', error)
} else {
console.log('onSignalSend Success', data)
}
})
}
}
// to receive the opentok signal
onSignalReceive = (signal) => {
console.log('onSignalReceive => ', JSON.parse(signal.data));
// based on signal data type you can do use switch or conditional statements
}
<OTSession
ref={this.otSession}
apiKey={apiKey}
sessionId={sessionId}
token={token}
onError={this.onSessionError}
eventHandlers={this.sessionEventHandlers}
onSignalRecieve={this.onSignalReceive}
getDevices={this.setDevices}
onMediaDevices={this.onMediaDevices}
checkScreenSharing={this.checkScreenSharing}>
<OTPublisher properties/>
<OTStreams>
<OTSubscriber properties/>
</OTStreams>
Here's a way to do this using functional component syntax.
import React, { useRef } from 'react';
import { OTSession, preloadScript } from 'opentok-react';
function MyComponent() {
const sessionRef = useRef();
const sendSignal = () => {
sessionRef.current.sessionHelper.session.signal(
{
type: 'TheSignalType',
data: 'TheData',
},
function (error) {
if (error) {
console.log('signal error: ' + error.message);
} else {
console.log('signal sent');
}
}
);
};
return (
<OTSession ref={sessionRef} apiKey={apiKey} sessionId={sessionId} token={token} eventHandlers={eventHandlers}>
// rest of your tokbox code here
<Button onClick={sendSignal}>Send Signal</Button>
</OTSession>
);
}
export default preloadScript(MyComponent);
In addition to #aiham answer, You can access the Opentok session Object getting the ref from the OTSession element and then send signals like below
otSession.current.sessionHelper.session.signal(
{
type: "msg",
data: text,
},
function(err, data) {
if (err) {
console.log(err.message);
} else {
console.log(data)
}
}
);
And signals can be received by adding a listener as follows;
otSession.current.sessionHelper.session.on("signal", (event) => {
console.log("i got", event);
});
In a React app I need to post data from a form. The post creates a new dashboard object. Once that's done, I need to immediately update a select dropdown in the component to include the newly added dashboard name. The axios documentation says it should be done like so:
function getUserAccount() {
return axios.get('/user/12345');
}
function getUserPermissions() {
return axios.get('/user/12345/permissions');
}
axios.all([getUserAccount(), getUserPermissions()])
.then(axios.spread(function (acct, perms) {
// Both requests are now complete
}));
So this is what I've done:
class DashboardForm extends Component {
saveDashboard() {
var siteId = this.state.siteId;
var self= this;
return axios.post('/path/to/save/dashboard' + siteId + '/dashboards', {
slug: this.refs.dashboardUrl.value,
name: this.refs.dashboardName.value,
}).then(function (response) {
self.setState({
dashboardId: response.data.dashboardId,
dashboardName: response.data.dashboardName,
submitMessage: (<p>Successfully Created</p>)
});
self.setUrl(siteId, response.data.dashboardId);
}).catch(function (error) {
console.log(error);
self.setState({
submitMessage: (<p>Failed</p>)
});
});
}
getAllDashboards(){
var self = this;
self.setState({siteId: this.props.selectedSiteID});
var getDashboardsPath = "path/to/get/dashboards/" + self.props.selectedSiteID + "/dashboards";
axios(getDashboardsPath, {
credentials: 'include',
method: 'GET',
cache: 'no-cache'
}).then(function (response) {
return response.data.dashboards;
}).then(function (arrDashboards) { //populate options for the select
var options = [];
for (var i = 0; i < arrDashboards.length; i++) {
var option = arrDashboards[i];
options.push(
(<option className="dashboardOptions" key={option.dashboardId} value={option.slug}>{option.name}</option>)
);
}
self.setState({
options: options
});
});
}
saveAndRepopulate() {
axios.all([saveDashboard(), getAllDashboards()])
.then(axios.spread(function (savedDashboard, listOfDashboards) {
// Both requests are now complete
}));
}
}
The saveAndRepopulate is called when the form submits.
The problem is that I get the following errors:
error 'saveDashboard' is not defined no-undef
error 'getAllDashboards' is not defined no-undef
I've tried doing
function saveDashboard() {
but then I get
Syntax error: Unexpected token, expected ( (175:13)
|
| function saveDashboard() {
| ^
How do I call these two functions? Also, am I going to need to change the promise (.then) from the individual calls to the saveAndPopulate?
Many thanks for any guidance.
First, as #Jaromanda X pointed out, you should call your inside components functions with this, and you need to bind these functions to this. There are multiple ways to do that, one of then is to bind it inside the component constructor like:
this.saveDashboard = this.saveDashboard.bind(this)
Other good thing to do is to return the axios call inside the saveDashboard() and getAllDashboards()
I got a issue here with methods firing not in the correct order.
I can't figure out how to make the this.props.history.pushState(null, '/authors'); wait in the saveAuthor() method.
Help will be greatly appreciated.
import React, { Component } from 'react';
import AuthorForm from './authorForm';
import { History } from 'react-router';
const source = 'http://localhost:3000/authors';
// History Mixin Component Hack
function connectHistory (Component) {
return React.createClass({
mixins: [ History ],
render () {
return <Component {...this.props} history={this.history}/>
}
})
}
// Main Component
class ManageAuthorPage extends Component {
state = {
author: { id: '', firstName: '', lastName: '' }
};
setAuthorState(event) {
let field = event.target.name;
let value = event.target.value;
this.state.author[field] = value;
return this.setState({author: this.state.author});
};
generateId(author) {
return `${author.firstName.toLowerCase()}-${author.lastName.toLowerCase()}`
};
// Main call to the API
postAuthor() {
fetch(source, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: this.generateId(this.state.author),
firstName: this.state.author.firstName,
lastName: this.state.author.lastName
})
});
};
// Calling Save author method but the this.props.history goes first rather than this.postAuthor();
saveAuthor(event) {
event.preventDefault();
this.postAuthor();
this.props.history.pushState(null, '/authors');
};
render() {
return (
<AuthorForm
author={this.state.author}
onChange={this.setAuthorState.bind(this)}
onSave={this.saveAuthor.bind(this)}
/>
);
}
}
export default connectHistory(ManageAuthorPage)
Fetch is an asynchronous function. Execution continues to the next line before the request is finished. You need to queue code to run after the request finishes. The best way to do that would be to make your postAuthor method return the promise, and then use the promise's .then method in the caller.
class ManageAuthorPage extends Component {
// ...
postAuthor() {
return fetch(source, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: this.generateId(this.state.author),
firstName: this.state.author.firstName,
lastName: this.state.author.lastName
})
});
};
saveAuthor(event) {
event.preventDefault();
this.postAuthor().then(() => {
this.props.history.pushState(null, '/authors');
});
};
// ...
}
If you're using a transpiler that supports ES7 async functions, then you could even do this in your saveAuthor method, which is equivalent and easier to read:
async saveAuthor(event) {
event.preventDefault();
await this.postAuthor();
this.props.history.pushState(null, '/authors');
};
So this is because your postAuthor method has an asynchronous call to fetch() inside of it. This is a time where you would want to pass in a function as a callback to the function, and then invoke that function inside the "completion" callback of the fetch call. The code would look something like this:
postAuthor(callback) {
fetch(source, {
/* Methods, headers, etc. */
}, () => {
/* Invoking the callback function that you passed */
callback();
});
);
saveAuthor(event) {
event.preventDefault();
/* Pass in a function to be invoked from within postAuthor when it is complete */
this.postAuthor(() => {
this.props.history.pushState(null, '/authors');
});
};