How to connect Spartacus with a custom API - spartacus-storefront

I have a custom API in the backend. There's no backend component for this. I need to call directly on the API. My question is I need to display the data inside a certain page slot. is this possible in spartacus? Please advise :)

I just need to create a CMSFlexComponent for a placeholder since cmsComponents can accept also a flex type:
cmsComponents: {
<item type | flex type>: {
component: <Angular Component>,
},
}

Related

best practice to change language dependent content coming from APIs

Background:
I have an SPA (Vuejs) and I implemented the localization both on frontend and backend side. The content is updated successfully without reloading the page when I change the language.
Problem:
But some contents (like product description in the selected language) are coming from an API which is not updated automatically when I change the language. If I refresh the page or call the API again, it works.
Question:
What is the best practice to change language dependent content coming from APIs without page refresh and calling manually all the APIs.
Thanks!
I believe there are many ways of doing what you want to do.
For instance. We have models that have some fields that can be localised, but we only support three languages and the fields are relatively short.
In that scenario, we just have the backend rest api return all localised versions of the string, e.g.:
GET /products could return:
[{
sku: '1',
name: {
en: 'Product name',
nl: 'Productnaam',
fr: 'Nom du produit'
}
}]
which we then just show in VueJs based on the route parameter... {{product.name[$route.params.locale] || product.name[en]}} (we have a composable method for this in stead, but you get the idea.
However, for some endpoints you might not always want to return all the localised versions of the server, just because it's too large (e.g. blogposts that you post in multiple languages while you support 5 languages might just generate a payload that you don't want to...)
In such scenario's, you can just watch for changes on your locale in vuejs, and fetch the localised version.
A third option, which I don't like myself, is have the "language switch" basically reload the entire page...
Either way - there's many ways to do what you need, it all depends on your use case, and your personal preference.
When using the vue-i18n Plugin, instead of sending the localised Text from the backend you could just send variables like exampleText and have them translated in your vue code with {{$t(yourLangVar)}} and this.$t(yourLangVar)
I managed to do it, I don't know if it is a best practice, but it is a working solution and can be useful for others:
App.vue:
I have a LocaleChanger component which emits localChanged on every language change.
the views are loaded into router-view so I added rerenderKey to its key. And when it is changed, everything will be rerendered in the current view(including the APIs of the corresponding components).
And since all of my APIs are called in it, They will be called again.
<template>
<LocaleChanger #localChanged="forceRerender"/>
<div id="nav">
<router-link :to="{ name: 'Home', params: {'lang':$i18n.locale}}">{{$t('pages.home')}}</router-link> |
</div>
<router-view :key="rerenderKey"></router-view>
</template>
<script>
data () {
return {
rerenderKey: 0
};
},
methods: {
async forceRerender(){
await this.$router.push({name: this.$route.name, params: { ...this.$route.params, lang: this.$i18n.locale }})
this.rerenderKey += 1;
},
}
</script>
And since my API-s need the Accept-language header, I added it in main.js to the axios.interceptors.request
axios.interceptors.request.use(
function (config) {
config.headers['Accept-Language'] = store.state.locale;
return config;
},
function (error) {
return Promise.reject(error);
}
);

SAP Spartacus How to show the custom components which is inside section

I have a section called "CustomTileSection" which is having some set of components (let's say 8 components but the number of components is not static and it is dynamic). I need to show the component in Spartacus. How can I get the data which is there for every component to iterate and show it in UI?
Each CMS Page Slot can contain dynamic amount of CMS Components, to show the slot with CMS Components inside you should:
Add the CMS Slot to the Page Template via Layout Config, for example (but if the Slot is custom):
LandingPage2Template: {
slots: [
'CustomSlot10',
],
},
More details here https://sap.github.io/spartacus-docs/page-layout/.
Map CMS Component to his Angular implementation via typeCode:
ConfigModule.withConfig({
cmsComponents: {
YourComponentTypeCode: {
component: AngularComponent;
}
}
});
More here https://sap.github.io/spartacus-docs/customizing-cms-components/.
Then you can take CMS Component's by the CmsComponentData service from AngularComponent's constructor:
public constructor(
public component: CmsComponentData<CmsYourComponent>
) {}
Additional details here https://sap.github.io/spartacus-docs/customizing-cms-components/#accessing-cms-data-in-cms-components.
Please note, that if all CMS Component have the same typeCode you need to map it only once, all another work Spartacus makes by self.

Vue/Nuxt: How to make a component be truly dynamic?

In order to use a dynamically-defined single page component, we use the component tag, thusly:
<component v-bind:is="componentName" :prop="someProperty"/>
...
import DynamicComponent from '#/components/DynamicComponent.vue';
...
components: {
DynamicComponent
},
props: {
componentName: String,
someProperty: null,
}
The problem is, this isn't really very dynamic at all, since every component we could ever possibly want to use here needs to be not only imported statically, but also registered in components.
We tried doing this, in order at least to avoid the need to import everything:
created() {
import(`#/components/${this.componentName}.vue`);
},
but of course this fails, as it seems that DynamicComponent must be defined before reaching created().
How can we use a component that is truly dynamic, i.e. imported and registered at runtime, given only its name?
From the documentation: Emphasis mine
<!-- Component changes when currentTabComponent changes -->
<component v-bind:is="currentTabComponent"></component>
In the example above, currentTabComponent can contain either:
the name of a registered component,
or a component’s options object
If currentTabComponent is a data property of your component you can simply import the component definition and directly pass it to the component tag without having to define it on the current template.
Here is an example where the component content will change if you click on the Vue logo.
Like this:
<component :is="dynamic" />
...
setComponentName() {
this.dynamic = () => import(`#/components/${this.componentName}.vue`);
},
Solution for Nuxt only
As of now its possible to auto-import components in Nuxt (nuxt/components). If you do so, you have a bunch of components ready to be registered whenever you use them in your vue template e.g.:
<MyComponent some-property="some-value" />
If you want to have truly dynamic components combined with nuxt/components you can make use of the way Nuxt prepares the components automagically. I created a package which enables dynamic components for auto-imported components (you can check it out here: #blokwise/dynamic).
Long story short: with the package you are able to dynamically import your components like this:
<NuxtDynamic :name="componentName" some-property="some-value" />
Where componentName might be 'MyComponent'. The name can either be statically stored in a variable or even be dynamically created through some API call to your backend / CMS.
If you are interested in how the underlying magic works you can checkout this article: Crank up auto import for dynamic Nuxt.js components
According to the official Documentation: Starting from v2.13, Nuxt can auto import your components when used in your templates, to activate this feature, set components: true in your configuration
you are talking about async components. You simply need to use the following syntax to return the component definition with a promise.
Vue.component('componentName', function (resolve, reject) {
requestTemplate().then(function (response) {
// Pass the component definition to the resolve callback
resolve({
template: response
})
});
})

Keeping logged in user information

I managed to log in with laravel passport. I have the token, great. But I want to keep logged in user information, name, avatar and so on.
My login procedure gets oauth token. In dashboard component I make api call for user data.
Should I keep them in global Vue object or use Vuex? Is it safe?
Some options you might consider
store data in cookies
use localStorage
keep everything in the root vue instance
keep everything in a wrapping vue component
My suggestion would be to store the auth token - that is actually required to successfully call your backend - in a cookie. This will make it super easy to access it with each and every request you send.
To store the user information I'd suggest to either create a wrapping component or use the root vue instance. The following example should clearify this.
wrapping home component (inline template)
data: function() {
return { userinfo: {} }
},
created: function() {
// load your user info
}
Then use it in your index.html / main view
<body>
<home inline-template>
<!-- any stuff here -->
<!-- pass the user prop to every component that is shown in the userinfo -->
<router-view :user="userinfo"></router-view>
</home>
</body>
Your components that are shown in the router-view can then access the user prop
example component
...
props: ['user'],
...
<template>
<span>{{ user.name }}</span>
</template>
...
IMPORTANT: to make this work you will also need to add props: true to the definition of your route. Everything is explained here in detail: https://router.vuejs.org/en/essentials/passing-props.html
Remark: If you don't want to load userdata in your wrapping component you can load it anywhere else and use an event bus to transfer the results to the wrapping component. However, you should always have only ONE source of truth regarding the user info. Store it only at a single place.
I found the session helper really easy to use and it solved the problem of globally using the oauth token:
See: https://laravel.com/docs/5.4/session, heading: The Global Session Helper

Vue-multiselect inconsistent reactive options

So I'm building an application using Laravel Spark, and therefore taking the opportunity to learn some Vue.js while I'm at it.
It's taken longer for me to get my head around it than I would have liked but I have nearly got Vue-multiselect working for a group of options, the selected options of which are retrieved via a get request and then updated.
The way in which I've got this far may well be far from the best, so bear with me, but it only seems to load the selected options ~60% of the time. To be clear - there are never any warnings/errors logged in the console, and if I check the network tab the requests to get the Tutor's instruments are always successfully returning the same result...
I've declared a global array ready:
var vm = new Vue({
data: {
tutorinstruments: []
}
});
My main component then makes the request and updates the variable:
getTutor() {
this.$http.get('/get/tutor')
.then(response => {
this.tutor = response.data;
this.updateTutor();
});
},
updateTutor() {
this.updateTutorProfileForm.profile = this.tutor.profile;
vm.tutorinstruments = this.tutor.instruments;
},
My custom multiselect from Vue-multiselect then fetches all available instruments and updates the available instruments, and those that are selected:
getInstruments() {
this.$http.get('/get/instruments')
.then(response => {
this.instruments = response.data;
this.updateInstruments();
});
},
updateInstruments() {
this.options = this.instruments;
this.selected = vm.tutorinstruments;
},
The available options are always there.
Here's a YouTube link to how it looks if you refresh the page over and over
I'm open to any suggestions and welcome some help please!
Your global array var vm = new Vue({...}) is a separate Vue instance, which lives outside your main Vue instance that handles the user interface.
This is the reason you are using both this and vm in your components. In your methods, this points to the Vue instance that handles the user interface, while vm points to your global array that you initialized outside the Vue instance.
Please check this guide page once more: https://v2.vuejs.org/v2/guide/instance.html
If you look at the lifecycle diagram that initializes all the Vue features, you will notice that it mentions Vue instance in a lot of places. These features (reactivity, data binding, etc.) are designed to operate within a Vue instance, and not across multiple instances. It may work once in a while when the timing is right, but not guaranteed to work.
To resolve this issue, you can redesign your app to have a single Vue instance to handle the user interface and also data.
Ideally I would expect your tutorinstruments to be loaded in a code that initializes your app (using mounted hook in the root component), and get stored in a Vuex state. Once you have the data in your Vuex state, it can be accessed by all the components.
Vuex ref: https://vuex.vuejs.org/en/intro.html
Hope it helps! I understand I haven't given you a direct solution to your question. Maybe we can wait for a more direct answer if you are not able to restructure your app into a single Vue instance.
What Mani wrote is 100% correct, the reason I'm going to chime in is because I just got done building a very large scale project with PHP and Vue and I feel like I'm in a good position to give you some advice / things I learned in the process of building out a PHP (server side) website but adding in Vue (client side) to the mix for the front end templating.
This may be a bit larger than the scope of your multiselect question, but I'll give you a solid start on that as well.
First you need to decide which one of them is going to be doing the routing (when users come to a page who is handling the traffic) in your web app because that will determine the way you want to go about using Vue. Let's say for the sake of discussion you decide to authenticate (if you have logins) with PHP but your going to handle the routing with Vue on the front end. In this instance your going to want to for sure have one main Vue instance and more or less set up something similar to this example from Vue Router pretending that the HTML file is your PHP index.php in the web root, this should end up being the only .php file you need as far as templating goes and I had it handle all of the header meta and footer copyright stuff, in the body you basically just want one div with the ID app.
Then you just use the vue router and the routes to load in your vue components (one for each page or category of page works easily) for all your pages. Bonus points if you look up and figure using a dynamic component in your main app.vue to lazy load in the page component based on the route so your bundle stays small.
*hint you also need a polyfill with babel to do this
template
<Component :is="dynamicComponent"/>
script
components: {
Account: () => import('./Account/Account.vue'),
FourOhFour: () => import('../FourOhFour.vue')
},
computed: {
dynamicComponent() {
return this.$route.name;
}
},
Now that we are here we can deal with your multiselect issue (this also basically will help you to understand an easy way to load any component for Vue you find online into your site). In one of your page components you load when someone visits a route lets say /tutor (also I went and passed my authentication information from PHP into my routes by localizing it then using props, meta fields, and router guards, its all in that documention so I'll leave that to you if you want to explore) on tutor.vue we will call that your page component is where you want to call in multiselect. Also at this point we are still connected to our main Vue instance so if you want to reference it or your router from tutor.vue you can just use the Vue API for almost anything subbing out Vue or vm for this. But the neat thing is in your main JS file / modules you add to it outside Vue you can still use the API to reference your main Vue instance with Vue after you have loaded the main instance and do whatever you want just like you were inside a component more or less.
This is the way I would handle adding in external components from this point, wrapping them in another component you control and making them a child of your page component. Here is a very simple example with multiselect pretend the parent is tutor.vue.
Also I have a global event bus running, thought you might like the idea
https://alligator.io/vuejs/global-event-bus/
tutor.vue
<template>
<div
id="user-profile"
class="account-content container m-top m-bottom"
>
<select-input
:saved-value="musicPreviouslySelected"
:options="musicTypeOptions"
:placeholder="'Choose an your music thing...'"
#selected="musicThingChanged($event)"
/>
</div>
</template>
<script>
import SelectInput from './SelectInput';
import EventBus from './lib/eventBus';
export default {
components: {
SelectInput
},
data() {
return {
profileLoading: true,
isFullPage: false,
isModalActive: false,
slackId: null,
isActive: false,
isAdmin: false,
rep: {
id: null,
status: '',
started: '',
email: '',
first_name: '',
},
musicTypeOptions: []
};
},
created() {
if (org.admin) {
this.isAdmin = true;
}
this.rep.id = parseInt(this.$route.params.id);
this.fetchData();
},
mounted() {
EventBus.$on('profile-changed', () => {
// Do something because something happened somewhere else client side.
});
},
methods: {
fetchData() {
// use axios or whatever to fetch some data from the server and PHP to
// load into the page component so say we are getting the musicTypeOptions
// which will be in our selectbox.
},
musicThingChanged(event) {
// We have our new selection "event" from multiselect so do something
}
}
};
</script>
this is our child Multiselect wrapper SelectInput.vue
<template>
<multiselect
v-model="value"
:options="options"
:placeholder="placeholder"
label="label"
track-by="value"
#input="inputChanged" />
</template>
<script>
import Multiselect from 'vue-multiselect';
export default {
components: { Multiselect },
props: {
options: {
type: [Array],
default() {
return [];
}
},
savedValue: {
type: [Array],
default() {
return [];
}
},
placeholder: {
type: [String],
default: 'Select Option...'
}
},
data() {
return {
value: null
};
},
mounted() {
this.value = this.savedValue;
},
methods: {
inputChanged(selected) {
this.$emit('selected', selected.value);
}
}
};
</script>
<style scoped>
#import '../../../../../node_modules/vue-multiselect/dist/vue-multiselect.min.css';
</style>
Now you can insure you are manging the lifecycle of your page and what data you have when, you can wait until you get musicTypeOptions before it will be passed to SelectInput component which will in turn set up Multiselect or any other component and then handle passing the data back via this.$emit('hihiwhatever') which gets picked up by #hihiwhatever on the component in the template which calls back to a function and now you are on your way to do whatever with the new selection and pass different data to SelectInput and MultiSelect will stay in sync always.
Now for my last advice, from experience. Resist the temptation because you read about it 650 times a day and it seems like the right thing to do and use Vuex in a setup like this. You have PHP and a database already, use it just like Vuex would be used if you were making is in Node.js, which you are not you have a perfectly awesome PHP server side storage, trying to manage data in Vuex on the front end, while also having data managed by PHP and database server side is going to end in disaster as soon as you start having multiple users logged in messing with the Vuex data, which came from PHP server side you will not be able to keep a single point of truth. If you don't have a server side DB yes Vuex it up, but save yourself a headache and wait to try it until you are using Node.js 100%.
If you want to manage some data client side longer than the lifecycle of a page view use something like https://github.com/gruns/ImmortalDB it has served me very well.
Sorry this turned into a blog post haha, but I hope it helps someone save themselves a few weeks.