I'm studying javascript and mithril.js 1.1.6. I'm writing down a simple web app in which users land on a page where he can login. Users who already did login land on a different page. I'm trying this using conditional routing, here is the main component:
const m = require("mithril");
...
import Eventbus from './whafodi/eventbus.js';
import WelcomePage from './ui/welcome.js';
import User from './model/user.js';
var eventbus = new Eventbus();
function MyApp() {
return {
usrAuth: function() {
m.route(document.body, "/", {
"/": { view: () => m("p", "hello")}
})
},
usrNotAuth: function() {
m.route(document.body, "/", {
"/": { render: v => m(WelcomePage, eventbus) }
})
},
oninit: function(vnode) {
vnode.state.user = new User();
eventbus.subscribe({
type: "login",
handle: function(action) {
vnode.state.user.token = action.token;
console.log(JSON.stringify(vnode.state.user));
}
});
},
view: function(vnode) {
if(vnode.state.user.token) {
this.usrAuth();
} else {
this.usrNotAuth();
}
}
}
};
m.mount(document.body, MyApp);
MyApp is the main component. It check if user has a token, then return the proper route. This is the component that is in charge to let users login:
const m = require("mithril");
const hellojs = require("hellojs");
function TopBar(node) {
var bus = node.attrs.eventbus;
function _login() {
hellojs('facebook').login({scope:'email'});
}
return {
oninit: function(vnode) {
hellojs.init({
facebook: XXXXXXX,
}, {
redirect_uri: 'http://localhost'
});
hellojs.on('auth.login', auth => {
var fbtoken = auth.authResponse.access_token;
m.request({
method:"POST",
url:"./myapp/login/fb/token",
data:auth.authResponse,
background: true
}).then(function(result){
console.log(result);
bus.publish({ type: "login", token: result.jwttoken });
m.route.set("/");
}, function(error){
console.log(error);
bus.publish({ type: "login", token: "" });
});
});
},
view: function(vnode) {
return m("div", [
m("button", { onclick: _login }, "Login")
]);
}
}
}
export default TopBar;
TopBar component occurs in the WelcomePage component mentioned in the main one. TopBar renders a button and use hello.js to login. It uses the EventBus bus parameter to tell main component user logged in (there is an handler in main component to update the user model). Once user logins, event is fired and main component updates the user model. Good. Now, how can trigger the main component to load the right route?
I read mithril'docs again and I found that RouteResolvers perfectly suit my needs. Here is an example:
var App = (function() {
var login;
function isLoggedIn(component) {
if(login) {
return component;
} else {
m.route.set("/hey");
}
}
return {
oninit: function(vnode) {
EventBus.subscribe({
type: "login",
handle: function(action) {
console.log("incoming action: " + JSON.stringify(action));
login = action.value;
}
});
},
oncreate: function(vnode) {
Foo.eventbus = EventBus;
Bar.eventbus = EventBus;
Hey.eventbus = EventBus;
m.route(document.body, "/hey", {
"/foo": {
onmatch: function(args, requestedPath, route) { return isLoggedIn(Foo); }
},
"/bar": {
onmatch: function(args, requestedPath, route) { return isLoggedIn(Bar); }
},
"/hey": Hey
});
},
view: function(vnode) {
return m("div", "home..");
}
};
})();
Eventbus is used to let components communicate with App. They fire events (login type events) that App can handle. I found convenient to pass Eventbus the way oncreate method shows, I can use Eventbus in each component's oncreate to let components fire events.
Related
I have java script component in home component with external js. I need to remove external js when page navigate to another page. Page does not refresh.
<script>
function initFreshChat() {
window.fcWidget.init({
token: "***",
host: "https://wchat.freshchat.com"
});
}
function initialize(i,t){var e;i.getElementById(t)?initFreshChat():((e=i.createElement("script")).id=t,e.async=!0,e.src="https://wchat.freshchat.com/js/widget.js",e.onload=initFreshChat,i.head.appendChild(e))}function initiateCall(){initialize(document,"freshchat-js-sdk")}window.addEventListener?window.addEventListener("load",initiateCall,!1):window.attachEvent("load",initiateCall,!1);
</script>
This is the external js: https://wchat.freshchat.com/js/widget.js
I need this because i need to keep this freshchat window in one page.
This can be done by putting any condition. But it will works if we refresh the page. Here pages are not refreshing at all.
Therefore I need to remove the external js when navigate to another pages. And mount back when came to this page.
You can wrap the script in side a Vue component life circle,
render
remove
refresh
whenever you need.
I found this code on codepen https://codepen.io/akccakcctw/pen/LBKQZE
Vue.component("fc-button", {
template: "#fcButton",
props: {
fc: {
type: Object,
default: {},
}
},
methods: {
openWidget: function() {
document.getElementById("fc_frame").style.visibility = "visible";
window.fcWidget.open();
}
}
});
const vm = new Vue({
el: "#app",
data: function() {
return {
fc: {
isInit: false,
},
};
},
mounted: function() {
var self = this;
window.fcSettings = {
token: "8d3a4a04-5562-4f59-8f66-f84a269897a1",
host: "https://wchat.freshchat.com",
config: {
cssNames: {
widget: "custom_fc_frame",
open: "custom_fc_open",
expanded: "custom_fc_expanded"
},
headerProperty: {
hideChatButton: true
}
},
onInit: function() {
window.fcWidget.on("widget:loaded", function() {
self.fc.isInit = true;
window.fcWidget.on("unreadCount:notify", function(resp) {
console.log(resp);
test = resp;
if (resp.count > 0) {
// document.getElementById('notify').classList.add('h-btn-notify');
document.getElementById("notify").style.visibility = "visible";
} else if (resp.count == 0) {
// document.getElementById('notify').classList.remove('h-btn-notify');
document.getElementById("notify").style.visibility = "hidden";
}
});
window.fcWidget.on("widget:closed", function() {
document.getElementById("fc_frame").style.visibility = "hidden";
document.getElementById("open_fc_widget").style.visibility =
"visible";
});
window.fcWidget.on("widget:opened", function(resp) {
document.getElementById("open_fc_widget").style.visibility =
"hidden";
});
});
}
};
}
});
I am struggeling with a proper solution which requires an advanced parent-child communication in vuejs. There can be many different parent components which has a logic how to save data. From the other side there will be only one child component which has a list of elements and a form to create new elements but it doesn't know how to save the data.
The question is: Is there any other way (better approach) to have the same functionality but to get rid of this.$refs.child links. For example I am wondering if I can just pass a function (SaveParent1(...) or SaveParent2(...)) to the child component. But the problem is the function contains some parent's variables which won't be available in child context and those variables could be changed during the runtime.
Just few clarifications:
The methods SaveParent1 and SaveParent2 in real life return
Promise (axios).
The child-component is like a CRUD which is used
everywhere else.
At the moment the communication looks like that: CHILD -event-> PARENT -ref-> CHILD.
Bellow is the example:
<div id="app">
<h2>😀Advanced Parent-Child Communication:</h2>
<parent-component1 param1="ABC"></parent-component1>
<parent-component2 param2="XYZ"></parent-component2>
</div>
Vue.component('parent-component1', {
props: { param1: { type: String, required: true } },
methods: {
onChildSubmit(p) {
// Here will be some logic to save the param. Many different parents might have different logic and all of them use the same child component. So child-component contains list, form and validation message but does not know how to save the param to the database.
var error = SaveParent1({ form: { p: p, param1: this.param1 } });
if (error)
this.$refs.child.paramFailed(error);
else
this.$refs.child.paramAdded(p);
}
},
template: `<div class="parent"><p>Here is parent ONE:</p><child-component ref="child" #submit="onChildSubmit"></child-component></div>`
});
Vue.component('parent-component2', {
props: { param2: { type: String, required: true } },
methods: {
onChildSubmit(p) {
// Here is a different logic to save the param. In prictice it is gonna be different requests to the server.
var error = SaveParent2({ form: { p: p, param2: this.param2 } });
if (error)
this.$refs.child.paramFailed(error);
else
this.$refs.child.paramAdded(p);
}
},
template: `<div class="parent"><p>Here is parent TWO:</p><child-component ref="child" #submit="onChildSubmit"></child-component></div>`
});
Vue.component('child-component', {
data() {
return {
currentParam: "",
allParams: [],
errorMessage: ""
}
},
methods: {
submit() {
this.errorMessage = "";
this.$emit('submit', this.currentParam);
},
paramAdded(p) {
this.currentParam = "";
this.allParams.push(p);
},
paramFailed(msg) {
this.errorMessage = msg;
}
},
template: `<div><ol><li v-for="p in allParams">{{p}}</li></ol><label>Add Param: <input v-model="currentParam"></label><button #click="submit" :disabled="!currentParam">Submit</button><p class="error">{{errorMessage}}</p></div>`
});
function SaveParent1(data) {
// Axios API to save data. Bellow is a simulation.
if (Math.random() > 0.5)
return null;
else
return 'Parent1: You are not lucky today';
}
function SaveParent2(data) {
// Axios API to save data. Bellow is a simulation.
if (Math.random() > 0.5)
return null;
else
return 'Parent2: You are not lucky today';
}
new Vue({
el: "#app"
});
There is also a live demo available: https://jsfiddle.net/FairKing/novdmcxp/
Architecturally I recommend having a service that is completely abstract from the component hierarchy and that you can inject and use in each of the components. With this kind of component hierarchy and architecture it is easy to run into these issues. It is important to abstract as much functionality and business logic from the components as possible. I think of components in these modern frameworks just merely as HTML templates on steroids, which should at most act as controllers, keeping them as dumb and as thin as possible so that you don't run into these situations. I do not know vue.js so I cannot give you the technical solution but hope this indication helps
I think I have found a solution. So no two ways communication. I can just pass a method and the child will do everything without communicating with parent. I am happy with that I am marking it as an answer. Thanks everyone for your help.
Let me please know what do you think guys.
Bellow is my solution:
<div id="app">
<h2>😀Advanced Parent-Child Communication:</h2>
<parent-component1 param1="ABC"></parent-component1>
<parent-component2 param2="XYZ"></parent-component2>
</div>
Vue.component('parent-component1', {
props: { param1: { type: String, required: true } },
computed: {
saveFunc() {
return function(p) { SaveParent1({ form: { p: p, param1: this.param1 } }); }.bind(this);
}
},
template: `<div class="parent"><p>Here is parent ONE:</p><child-component :saveFunc="saveFunc"></child-component></div>`
});
Vue.component('parent-component2', {
props: { param2: { type: String, required: true } },
computed: {
saveFunc() {
return function(p) { SaveParent2({ form: { p: p, param2: this.param2 } }); }.bind(this);
}
},
template: `<div class="parent"><p>Here is parent TWO:</p><child-component :saveFunc="saveFunc"></child-component></div>`
});
Vue.component('child-component', {
props: {
saveFunc: { type: Function, required: true }, // This is gonna be a Promise in real life.
},
data() {
return {
currentParam: "",
allParams: [],
errorMessage: ""
}
},
methods: {
submit() {
this.errorMessage = "";
var error = this.saveFunc(this.currentParam);
if (error)
this.paramFailed(error);
else
this.paramAdded(this.currentParam);
},
paramAdded(p) {
this.currentParam = "";
this.allParams.push(p);
},
paramFailed(msg) {
this.errorMessage = msg;
}
},
template: `<div><ol><li v-for="p in allParams">{{p}}</li></ol><label>Add Param: <input v-model="currentParam"></label><button #click="submit" :disabled="!currentParam">Submit</button><p class="error">{{errorMessage}}</p></div>`
});
function SaveParent1(data) {
console.log(data);
// Axios API to save data
if (Math.random() > 0.5)
return null;
else
return 'Parent1: You are not lucky today';
}
function SaveParent2(data) {
console.log(data);
// Axios API to save data
if (Math.random() > 0.5)
return null;
else
return 'Parent2: You are not lucky today';
}
new Vue({
el: "#app"
});
The demo link: https://jsfiddle.net/FairKing/novdmcxp/126/
I am in the process of integrating AmazonPay into a React SPA. The classic integration relies on script tags and callbacks (docs).
Here is one example from the button widget:
<head>
<script type='text/javascript'>
window.onAmazonLoginReady = function() {
amazon.Login.setClientId('CLIENT-ID');
};
window.onAmazonPaymentsReady = function() {
showButton();
};
</script>
<script async="async" src='https://static-na.payments-amazon.com/OffAmazonPayments/us/sandbox/js/Widgets.js'>
</script>
</head>
<body>
. . .
<div id="AmazonPayButton">
</div>
...
<script type="text/javascript">
function showButton(){
var authRequest;
OffAmazonPayments.Button("AmazonPayButton", "SELLER-ID", {
type: "TYPE",
color: "COLOR",
size: "SIZE",
authorization: function() {
loginOptions = {scope: "SCOPES",
popup: "POPUP-PARAMETER"};
authRequest = amazon.Login.authorize (loginOptions,
"REDIRECT-URL");
},
onError: function(error) {
// your error handling code.
// alert("The following error occurred: "
// + error.getErrorCode()
// + ' - ' + error.getErrorMessage());
}
});
};
</script>
. . .
<script type="text/javascript">
document.getElementById('Logout').onclick = function() {
amazon.Login.logout();
};
</script>
</body>
When using React, the div with id="AmazonPayButton" isn't on the page until React mounts the div, causing the window.showButton() function to fail.
To circumvent this issue, I've wrapped the function showButton() definition inside window.showButton():
window.onAmazonPaymentsReady = function() {
window.showButton = function () {
var authRequest;
// eslint-disable-next-line no-undef
OffAmazonPayments.Button("AmazonPayButton", "%REACT_APP_AMAZON_SELLER_ID_SANDBOX%", {
type: "PwA",
color: "Gold",
size: "medium",
authorization: function () {
loginOptions = {
scope: "profile postal_code payments:widget payments:shipping_address",
popup: true
};
authRequest = amazon.Login.authorize(loginOptions, "%PUBLIC_URL%/pay-with-amazon");
},
onError: function (error) {
console.log(error.toString())
}
});
};
};
The component which contains the AmazonPay div can now be called on componentDidMount:
import React, {Component} from 'react'
class AmazonMethod extends Component {
componentDidMount () {
window.showButton()
}
render() { return <div id="AmazonPayButton"></div>}
}
export default AmazonMethod
I am confused how to access the onError callback from inside my React component. How do I listen for the callback and respond appropriately?
This question applies to AddressWidget and WalletWidget as well; they all rely on script tag callbacks.
Update:
I've written a post which summarizes how to integrate AmazonPay with client side React.
Why don't you just pass in a function to your showButton function in componentDidMount that onError can call?
Something like this:
window.onAmazonPaymentsReady = function() {
window.showButton = function (errorFunc) {
var authRequest;
// eslint-disable-next-line no-undef
OffAmazonPayments.Button("AmazonPayButton", "%REACT_APP_AMAZON_SELLER_ID_SANDBOX%", {
type: "PwA",
color: "Gold",
size: "medium",
authorization: function () {
loginOptions = {
scope: "profile postal_code payments:widget payments:shipping_address",
popup: true
};
authRequest = amazon.Login.authorize(loginOptions, "%PUBLIC_URL%/pay-with-amazon");
},
onError: function (error) {
console.log(error.toString())
errorFunc(error)
}
});
};
};
import React, {Component} from 'react'
class AmazonMethod extends Component {
componentDidMount () {
window.showButton(this.errorFunc)
}
errorFunc = (error) => {
console.log(error);
this.setState({
amazonError: error
});
}
render() { return <div id="AmazonPayButton"></div>}
}
export default AmazonMethod
I have two demos for same library under repo, with demo.
The main difference is that, one is for browser use and the other is for node use.
However browser one will have error
index.js:1 Uncaught SyntaxError: Identifier 'HeaderComp' has already been declared
What is the main cause?
Update:
Please keep in mind I do not declare a variable twice! I also tried to add console log at the top to ensure the script is executed once!
var HeaderComp = {
name: 'HeaderComp',
template: `
Back
{{ r.title }}
`,
mixins: [VueTopDown.VueTopDownItem],
computed: {
routes () {
return [
{ href: '/page', title: 'Page' },
{ href: '/hello-vue', title: 'HelloVue' }
]
}
}
}
var FooterComp = {
name: 'FooterComp',
template: `{{ vueFooter }}`,
mixins: [VueTopDown.VueTopDownItem],
data () {
return {
vueFooter: 'This is Vue Footer'
}
}
}
var ContentComp = {
name: 'ContentComp',
template: ``,
mixins: [VueTopDown.VueTopDownItem],
computed: {
innerHTML () {
var root = document.createElement('div')
root.innerHTML = this[VTDConstants.OUTER_HTML]
return root.querySelector('*').innerHTML
}
}
}
var HelloVue = {
template: `Hello Vue`,
props: ['clazz'],
inheritAttrs: false
}
var Page = {
template: ``,
props: ['clazz', 'innerHTML'],
inheritAttrs: false
}
var router = new VueRouter([
{ path: '/hello-vue', component: HelloVue },
{ path: '/page', component: Page },
{ path: '*', redirect: '/page' }
])
var inst = new Vue({
router,
mixins: [VueTopDown.VueTopDown],
components: {
HeaderComp: HeaderComp,
FooterComp,
ContentComp
},
data () {
return {
[VueTopDown.VTDConstants]: {
'header': HeaderComp,
'footer': FooterComp,
'.content': ContentComp
}
}
}
})
inst.$mount('#app')
Also keep in mind that similar code works fine in node environment but fails in browser!
Doesn't occur if commenting out inst.$mount('#app')
Expect
The expected behavior of browser should be same as that of node.
Given:
<parent-element>
<sibling-a></sibling-a>
<sibling-b></sibling-b>
</parent-element>
How can I get access to a click event on a button in siblingA to change some value to another in sibling-b using $emit(…)?
#craig_h is correct, or you can use $refs like:
<parent-element>
<sibling-a #onClickButton="changeCall"></sibling-a>
<sibling-b ref="b"></sibling-b>
</parent-element>
In parent methods:
methods: {
changeCall() {
this.$refs.b.dataChange = 'changed';
}
}
In siblingA:
Vue.component('sibling-a', {
template: `<div><button #click="clickMe">Click Me</button></div>`,
methods: {
clickMe() {
this.$emit('onClickButton');
}
}
});
In siblingB:
Vue.component('sibling-b', {
template: `<div>{{dataChange}}</div>`,
data() {
return {
dataChange: 'no change'
}
}
});
For that you can simply use a global bus and emit your events on to that:
var bus = new Vue();
Vue.component('comp-a', {
template: `<div><button #click="emitFoo">Click Me</button></div>`,
methods: {
emitFoo() {
bus.$emit('foo');
}
}
});
Vue.component('comp-b', {
template: `<div>{{msg}}</div>`,
created() {
bus.$on('foo', () => {
this.msg = "Got Foo!";
})
},
data() {
return {
msg: 'comp-b'
}
}
});
Here's the JSFiddle: https://jsfiddle.net/6ekomf2c/
If you need to do anything more complicated then you should look at Vuex