Spartacus Configurable Product Integration: Use custom Form Elements - spartacus-storefront

I am currently experiencing issues with the Configurable Product Integration of Spartacus. So far I have set up the storefront to show my configurable products, which is working quite well.
Now I am trying to customize a form element, let's say an input field to insert a custom suffix that is dynamically added by the backend. Specifically, I am trying to replace the cx-configurator-attribute-input-fieldinside https://github.com/SAP/spartacus/blob/develop/feature-libs/product-configurator/rulebased/components/form/configurator-form.component.html
So far I have tried to import it in the module:
ConfigModule.withConfig({
cmsComponents: {
ConfiguratorAttributeInputField: {
component:CustomConfiguratorComponent
}
}
})
which is not working, probably because the component is not a static CmsComponent.
I also tried importing it via outlet definition in typescript:
export class CustomConfiguratorComponent implements OnInit{
constructor(private componentFactoryResolver: ComponentFactoryResolver, private outletService: OutletService){}
ngOnInit(){
const factory = this.componentFactoryResolver.resolveComponentFactory(CustomInputFieldComponent);
this.outletService.add('cx-configurator-attribute-input-field', factory);
console.log("REGISTERED");
}
}
which is also not working.
When I add the outlet via ng-template to a template reference, I can see the component, so the import of the component should be working correctly:
<ng-template cxOutletRef="VariantConfigurationTemplate" cxOutletPos="before">
<app-custom-input-field></app-custom-input-field>
</ng-template>
How can I get this to work, so that I can replace a dynamically added Spartacus component? Specifically the ConfiguratorAttributeInputFieldComponent (https://github.com/SAP/spartacus/blob/develop/feature-libs/product-configurator/rulebased/components/attribute/types/input-field/configurator-attribute-input-field.component.ts)

Related

How to override cx-item-counter in spartacus

I am trying to customize the product increment or decrement quantity from OOTB to have my own implementation. Below is the screenshot.
I tried to create a new component in my custom store and used the extend feature of angular as this is not a cms component.
My Custom component is not getting rendered instead it renders the cx-item-counter.
import { ItemCounterComponent } from '#spartacus/storefront';
import {
Component,
OnDestroy,
OnInit,
} from '#angular/core';
#Component({
selector: 'custom-item-counter',
templateUrl: './cart-counter.component.html',
export class CartCounter extends ItemCounterComponent {
}
Is there any way I can achieve to override this?
Firstly, have a look at how the component is setup in the official spartacus repo:
https://github.com/SAP/spartacus/tree/release/4.0.0/projects/storefrontlib/src/shared/components/item-counter
I imagine you would need to create a module and have the module import the component. To render your custom version, you would need to use in the component that you import your custom component in.
Wherever you want your custom module/component to be imported, replace ItemCounterModule.
Currently the add to cart component imports it directly:
https://github.com/SAP/spartacus/tree/release/4.0.0/projects/storefrontlib/src/cms-components/cart/add-to-cart
If you are not overriding the add to cart component, then I am not sure you can achieve what you are looking to achieve, since the spartacus version of the item counter is being imported directly. You might need to extend the add to cart component as well so that you can change to .
This would be my approach. I have not tested it. Perhaps this at least points you in the right direction.

Nuxt class-based services architecture (register globally; vs manual import)

In my Nuxt app I'm registering app services in a plugin file (e.g. /plugins/services.js) like this...
import FeatureOneService from '#/services/feature-one-service.js'
import FeatureTwoService from '#/services/feature-two-service.js'
import FeatureThreeService from '#/services/feature-three-service.js'
import FeatureFourService from '#/services/feature-four-service.js'
import FeatureFiveService from '#/services/feature-five-service.js'
export default (ctx, inject) => {
inject('feature1', new FeatureOneService(ctx))
inject('feature2', new FeatureTwoService(ctx))
inject('feature3', new FeatureThreeService(ctx))
inject('feature4', new FeatureFourService(ctx))
inject('feature5', new FeatureFiveService(ctx))
}
After doing this I can access any of my service on vue instance like this.$feature1.someMethod()
It works but I've once concern, that is, this approach loads all services globally. So whatever page the user visits all these services must be loaded.
Now I've 20+ such services in my app and this does not seem optimal approach to me.
The other approach I was wondering is to export a singleton instance within each service class and import this class instance in any component which needs that service.
So basically in my service class (e.g. feature-one-service.js) I would do like to do it like this..
export default new FeatureOneService() <---- I'm not sure how to pass nuxt instance in a .js file?
and import it my component where it is required like so...
import FeatureOneService from '#/services/feature-one-service.js'
What approach do you think is most feasible? if its the second one, then how to pass nuxt instance to my singleton class?
Yep, loading everything globally is not optimal in terms of performance.
You will need to either try to use JS files and pass down the Vue instance there.
Or use mixins, this is not optimal but it is pretty much the only solution in terms of reusability with Vue2.
Vue3 (composition API) brings composables, which is a far better approach regarding reusability (thing React hooks).
I've been struggling a lot with it and the only solution is probably to inject services to the global Vue instance at the component/page level during the initialisation (in created hook), another option is to do that in the middleware (or anywhere else where you have access to the nuxt context. Otherwise you won't be able to pass nuxt context to the service.
I usually set up services as classes, call them where necessary, and pass in the properties of the context which the class depends on as constructor arguments.
So for example, a basic MeiliSearchService class might look like:
export class MeilisearchService {
#client: MeiliSearch
constructor($config: NuxtRuntimeConfig) {
super()
this.#client = new MeiliSearch({
host: $config.services.meilisearch.host,
apiKey: $config.services.meilisearch.key
})
}
...
someMethod() {
let doSomething = this.#client.method()
...
}
...
}
Then wherever you need to use the service, just new up an instance (passing in whatever it needs) and make it available to the component.
data() {
const meiliSearchService = new MeiliSearchService(this.$config)
return {
meiliSearchService,
results,
...
}
},
methods: {
search(query) {
...
this.results = this.meiliSearchService.search(query)
...
}
}
As I'm sure you know, some context properties are only available in certain Nuxt life-cycle hooks. I find most of what I need is available everywhere, including $config, store, $route, $router etc.
Don't forget about Vue's reactivity when using this approach. For example, using a getter method on your service class will return the most recent value only when explicitly called. You can't, for example, stick the getter method in a computed() property and expect reactivty.
<div v-for='result in latestSearchResults'>
...
</div>
...
computed: {
latestSearchResults() {
return this.#client.getLatestResults()
}
}
Instead, call the method like:
methods: {
getLatestResults() {
return this.#client.getLatestResults()
}
}

How to acced to google object in vue 2?

I'm trying to use the google maps API at my vue2 project and I have tried some ways that have failed. After using the vue2googlemaps module and the node module from google I have decided to use the CDN directly and add it to the index page. My problem now is that to acced to the google object, for example, to create a Marker or something like that, I need to use this.marker = new window.google.maps.Marker() for example, but in the tutorials I have seen, everyone uses directly the google object and never uses that window. I can`t understand why it happens. It would be appreciated if someone shows me the correct way to import or use this library on google.
It's because your template's code is compiled and executed in your component instance (a.k.a vm) 's scope, not in the global (a.k.a. window) scope.
To use google directly in your template you could add the following computed:
computed: {
google: () => window.google
}
If your problem is not having google defined in the component's <script>, a simple solution is to add it as a const at the top:
import Vue from 'vue';
const google = window.google;
export default Vue.extend({
computed: {
google: () => google // also make it available in template, as `google`
}
})
An even more elegant solution is to teach webpack to get google from the window object whenever it's imported in any of your components:
vue.config.js:
module.exports = {
configureWebpack: {
externals: {
google: 'window.google'
}
}
}
This creates a google namespace in your webpack configuration so you can import from it in any of your components:
import google from 'google';
//...
computed: {
google: () => google // provide it to template, as `google`
}
Why do I say it's more elegant?
Because it decouples the component from the context and now you don't need to modify the component when used in different contexts (i.e: in a testing environment, which might not even use a browser, so it might not have a window object, but a global instead; all you'd have to do in this case is to define a google namespace in that environment and that's where your component will get its google object from; but you wouldn't have to tweak or mock any of the component's methods/properties).

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.js 2 how use components in ES2015 webpack

I am trying to use vue-components in a webpack Typescript project but it doesn't seem to be working. I don't get any errors during the build and run, but the component HTML is never inserted into the output - I can just see the HTML source of the component instead i.e. .
My project is an ES2015 using Vue2 in VS.Net 2017. My component looks like this:
import Vue from 'vue'
import Component from 'vue-class-component'
// The #Component decorator indicates the class is a Vue component
#Component({
// All component options are allowed in here
template: '<button #click="onClick">Click!</button>'
})
export default class MyHeader extends Vue {
// Initial data can be declared as instance properties
message: string = 'Hello!'
// Component methods can be declared as instance methods
onClick(): void {
window.alert(this.message)
}
}
I have tried the official reference guide to register the component and use it. When I look at the vue-component example, it uses the same format as my project so I added the markup and properties to my Typescript class definition:
import Vue from 'vue';
import Component from 'vue-class-component';
import MyHeader from './MyHeader';
#Component({
components: {
MyHeader
}
})
export default class GetDataComponent extends Vue {
<...rest of class...>
}
but in my project the "components:" section is squiggly-underline-red with the message:
Object literal may only specify known properties, but 'components'
does not exist in type 'VueClass'. Did you mean to write
'component'?
Every example I have seen with vue-component (such as this one) uses the "components:" option in the #Component to register and use their Vue component, but in my project it doesn't seem to like it. I have also tried global registration of the component (such as this one) which includes the line:
// Register the component globally
Vue.component(my-header', MyHeader)`
but in that case I get an error like this:
Type 'typeof MyHeader' is not assignable to type 'AsyncComponent'
The Vue file works (without the Component added) and all content is rendered correctly. It's getting the Component included that doesn't work - I either get Design-time errors per above, or nothing appears in the output at all.
Is my import wrong? Or the format of the #Component? I get the feeling I am doing something that is very basic, very wrong...