A project I have been working on has been in need of an improved billing system, with the release of the cashier-paddle package I was able to add some subscriptions to the app.
Working with the package, one can see that it's relatively easy and fluent to get paddle / cashier working in your Laravel app. And this is great!
Noticing the load times on generating links
I noticed after a while that sometimes the loading of the payment links took a while. Taking into account the API is requested for each link. Making in turn the response from the server slow, ... ... ... waiting for each link to be generated.
Obviously, the immediate thing here is that if this isn't an SPA that doesn't have to get new pages, you will notice. And you would notice on an SPA 🤷♂️
Now, this is greatly exaggerated if you are showing a bunch of paddle links for a user to choose. That sucks really.
So I decided to make it a little more enjoyable to load up a bunch of possible subscriptions for an end user, and also make the vue component renderless. This makes it easier for me to port it between a few projects.
So off to work on a 'simplification'.
First thing, the original blade component file:
<a href="#!" data-override="{{ $url }}" {{ $attributes->merge(['class' => 'paddle_button']) }}>
{{ $slot }}
</a>
Basically, the important things here are: href="#!"
, data-override=""
and the css class paddle_button
Awesome, so when the component is made we just need to make sure those are there in either out component or the added ones.
Please note, there isn't any need to move away from the blade component, it's an awesome way of rendering the links each time. For this instance it didn't always work
Renderless Paddle Subscription Component
Now to the component, it is in Vuejs because currently that is the frontend it was made for :)
We can create a js file called Subscriptions.js
under your js folder, your choice. Mine are in a components folder.
We know that we need to hold the links that are retrieved, a loading state and error state, so lets get those into the component
export default {
data() {
return {
payLinks: [],
loaded: false,
error: false,
},
},
render() {
return this.$slots.default[0];
}
}
This would be our basic component that we can return. But, I wanted to pass some things down to the elements in the slot area to use.
So we make use of the $scopedSlots
property, this allows us to pass data to the slots, and then pick up those in our html side.
export default {
data() {
return {
payLinks: [],
loaded: false,
error: false,
},
},
render() {
- return this.$slots.default[0];
+ return this.$scopedSlots.default({
+ payLinks: this.payLinks,
+ loaded: this.loaded,
+ error: this.error,
+ })
}
}
Fetching the Paddle PayLinks from the backend over an API endpoint
Now the fun part, getting the paddle links from the server and loading them into the component
I initially had the url for the endpoint hard coded, but then realized it would be better to have it as a prop, can then use something like: endpoint="{{ route('api.billing.links') }}"
.
Change the component to include the following:
export default {
data() {
...
},
+ props: {
+ /**
+ * The api endpoint to fetch the billing info
+ */
+ endpoint: {
+ type: String,
+ default: '/api/settings/billing'
+ }
+ },
+ methods: {
+ fetch() {
+ axios.get(this.endpoint)
+ .then(({
+ data: { data }
+ }) => {
+ this.payLinks = data;
+ this.loaded = true;
+ this.error = false;
+ })
+ .catch(error => {
+ this.loaded = false;
+ this.error = true;
+ throw error;
+ });
+ }
+ },
render() {
return this.$scopedSlots.default({
payLinks: this.payLinks,
loaded: this.loaded,
error: this.error,
+ fetch: this.fetch
});
}
}
The method fetch
uses the endpoint value in the axios request, this in turn loads the data from the response into the payLinks
url.
We also add the fetch method to the output of the slot scope, enabling us to trigger a reload from outside the component
Adding some messages and feedback
export default {
data() {
return {
payLinks: [],
loaded: false,
error: false,
+ message: 'Loading Subscriptions ...',
}
},
... // props
methods: {
fetch() {
axios.get(this.endpoint)
.then(({
data: { data }
}) => {
this.payLinks = data;
this.loaded = true;
this.error = false;
})
.catch(error => {
this.loaded = false;
this.error = true;
+ this.message = 'Failed to load current subscriptions';
throw error;
});
}
},
render() {
return this.$scopedSlots.default({
payLinks: this.payLinks,
loaded: this.loaded,
error: this.error,
+ message: this.message,
fetch: this.fetch
});
}
}
Loading Paddle after getting the links.
When using the Laravel cashier-paddle package, you use the @paddleJs
directive, this loads buttons that are already there. Not our vue rendered checkout button.
When checking the methods available on the Paddle object that is loaded you find that the Paddle JS file has some methods that are callable, and one that instantiates the buttons, and we can call it, awesome stuff.
So, what we do is to make use of the fact that because we are rendering the data inside the component after fetching it, we can rely on the updated
method to reload the Paddle buttons. This is done using Paddle.Button.load()
So what we can do is add a new method and stick that into the updated handler:
export default {
+ updated() {
+ this.loadPaddle()
+ },
methods: {
+ loadPaddle() {
+ // check if paddle is loaded.
+ if (Paddle) {
+ try {
+ Paddle.Button.load();
+ } catch (error) {
+ console.error(error);
+ }
+ }
+ }
},
}
Loading up the Paddle links
Right so our file now has the needed methods and data properties, the final bit, and probably really, really important is the css paddle_button
.
Now lets look at the mounted method, this is where we are going to call all the methods and add the paddle_button
class to our links.
mounted() {
this.$nextTick(() => {
this.fetch();
this.$el.querySelectorAll('a')
.forEach(element => {
if (element.href === '#!') {
element.classList.add('paddle_button');
}
});
});
},
Basically, after the component is mounted, we wait till the next tick from vue and all that, fetch the links from the backend.
We then use some good ol' Javascript 😁. Using the querySelectorAll('a')
function, we can get all the links that happen to be in the slot area, loop through them, then add the class paddle_button
on any link that has the href of #!
. That way we don't load up other random urls to have the paddle stuff.
Final JS file
Below is the final component file, a lot nicer than the bunch of diff markings all over...
/**
* Paddle Subscription button renderless component.
*
* @author ReeceM
* @copyright MIT
* @filename Subscriptions.js
*/
// import axios from 'axios'; // optional..
export default {
data() {
return {
payLinks: [],
loaded: false,
error: false,
message: 'Loading Subscriptions ...',
}
},
props: {
/**
* The api endpoint to fetch the billing info
*/
endpoint: {
type: String,
default: '/api/settings/billing'
}
},
mounted() {
this.$nextTick(() => {
this.fetch();
this.$el.querySelectorAll('a')
.forEach(element => {
if (element.href === '#!') {
element.classList.add('paddle_button');
}
});
});
},
updated() {
this.loadPaddle()
},
methods: {
/**
* Fetch the Paddle Paylinks from the server.
*/
fetch() {
this.error = false;
this.loaded = false;
axios.get(this.endpoint)
.then(({
data: { data }
}) => {
this.payLinks = data;
this.loaded = true;
})
.catch(error => {
this.message = 'Failed to load current subscriptions';
this.loaded = false;
this.error = true;
throw error;
});
},
/**
* reload the Paddle buttons
*/
loadPaddle() {
Paddle ? Paddle.Button.load() : console.warn('Up a creek!');
}
},
render() {
return this.$scopedSlots.default({
payLinks: this.payLinks,
loaded: this.loaded,
error: this.error,
message: this.message,
fetch: this.fetch
});
}
}
Example usage
Below is an example that uses the laravel blade file, and then builds up a table with the buttons and the paylinks. The styling is in Bulma.
<subscriptions>
<template v-slot="{payLinks, message, loaded, error, fetch}">
<table class="table is-striped is-fullwidth table-bordered">
<tbody>
<tr v-if="!loaded">
<td v-text="message"></td>
<td v-if="error">
<div class="notification is-warning">
<button
@click=fetch()
class="button is-small is-outlined">
Retry
</button>
</div>
</td>
</tr>
<tr v-else
v-for="payLink in payLinks"
:key="payLink.title" >
<td>@{{ payLink['title'] }} Plan</td>
<td>terms</td>
<td>
<a
href="#!"
:data-override="payLink['link']"
data-theme="none"
class="button is-info"
:class="{'is-success': payLink['current']}" >
@{{payLink['current'] === true ? 'Subscribed' : 'Subscribe'}}
</a>
</td>
</tr>
</tbody>
</table>
</template>
</subscriptions>
To make use of the slot scope, we need to use a <template>
element, then add the v-slot="{payLinks, message, loaded, error, fetch}"
attribute, this allows us to access the needed data.
From there we can use the stuff inside the blade file to change how we render the Subscription links, this is nice because we don't have to recompile the JS file every time we need to make a change to a bit of wording.
Just don't forget the @
before the handlebar tags, or you going to end up with a bunch of errors from the blade engine.
I will share the follow up article to this on an example backend controller to get the links with.
I hope you enjoyed the post, please share any feedback you have on it or thoughts, you can give me a shout on twitter with @iexistin3d
.
0_o
Top comments (1)
Hey there 👋, if you happen to get this far. Maybe leave a comment if you have some improvement suggestions or an alternative approach to this :)