Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Best practices first version #1455

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
11 changes: 11 additions & 0 deletions _data/navigation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,17 @@ docs:
- title: Storefront Development Guide
url: /dev-guide/
children:
- title: "Best practices ➢"
url: /best-practises/
subchildren:
- title: "Layout best practises➢"
url: /layout-best-practices/
- title: "I18n best practises➢"
url: /i18n-best-practices/
- title: "Structure best practises➢"
url: /structure-best-practices/
- title: "Extending best practises➢"
url: /extending-best-practices/
- title: "Accessibility ➢"
url: /a11y/
subchildren:
Expand Down
13 changes: 13 additions & 0 deletions _pages/dev/best-practises/best-practises.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
title: Best practises
---

This is a landing page for developers best practises topics. It includes the following:

- [{% assign linkedpage = site.pages | where: "name", "layout-best-practices.md" %}{{ linkedpage[0].title }}]({{ site.baseurl }}{% link _pages/dev/best-practises/layout-best-practices.md %})
- [{% assign linkedpage = site.pages | where: "name", "i18n-best-practices.md" %}{{ linkedpage[0].title }}]({{ site.baseurl }}{% link _pages/dev/best-practises/i18n-best-practices.md %})
- [{% assign linkedpage = site.pages | where: "name", "structure-best-practices.md" %}{{ linkedpage[0].title }}]({{ site.baseurl }}{% link _pages/dev/best-practises/structure-best-practices.md %})
- [{% assign linkedpage = site.pages | where: "name", "extending-best-practices.md" %}{{ linkedpage[0].title }}]({{ site.baseurl }}{% link _pages/dev/best-practises/extending-best-practices.md %})
- [{% assign linkedpage = site.pages | where: "name", "styles-best-practices.md" %}{{ linkedpage[0].title }}]({{ site.baseurl }}{% link _pages/dev/best-practises/styles-best-practices.md %})

All of the above instructions are only suggestions on how to use Spartacus from the core team members.
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
title: Extending Best Practices
---

The Spartacus library gives us great opportunities. Many Spartacus elements can be reused and some of them just need to be carefully extended in our own way.

Extending the elements should be the basic action we will perform in Spartacus implementation projects.

Each time you have to implement a new feature or a single functionality, find it or something similar in the Spartacus Code and analyse how much you can reuse.

### Extending components

In many cases, we will only need to implement the component appearance because of the designs. What we should do is to create our own component, extends it with Spartacus component and use our own template and styles. If we have to add any new method or override existing we can to it in our own component.

```
@Component({
selector: 'custom-cart-totals',
templateUrl: './cart-totals.component.html',
styleUrls: ['./cart-totals.component.scss']
})
export class CustomCartTotalsComponent extends CartTotalsComponent implements OnInit {

constructor(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're not changing the dependencies of the component, we can omit constructor overriding because it would be inherited. Omitting the constructor, in this case, has the benefit that after the Spartacus version upgrade we don't need to do any modifications to our custom code.

At the same time it would be valuable to add an additional example where we do need to add an additional dependency to our component.

activeCartService: ActiveCartService
) {
super(activeCartService)
}

ngOnInit(): void {
super.ngOnInit();
// additional action
}

customMethod(): void {
// custom method
}
}
```

### Extending adapters, services, serializers, guards

If we need to extend any adapter/service/serializer/guard we should do it in a similar way to the component. Creating own custom adapter/service/serializer/guard and extending the Spartacus adapter/service/serializer/guard with our own. Additionally, we have to provide it in our module.

```
@NgModule({
imports: [
CommonModule
],
providers: [
{
provide: CartAdapter,
useClass: CustomCartAdapter,
},
{
provide: ActiveCartService,
useClass: CustomActiveCartService
},
{
provide: SiteContextUrlSerializer,
useClass: CustomSiteContextUrlSerializer,
},
{
provide: AuthGuard,
useClass: CustomAuthGuard,
},
]
})
export class CartTotalModule { }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to be mentioned somewhere else but I always suggest to add a custom prefix to all the elements. Here it is easy to mistake custom CartTotalsModule with the core one.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to be mentioned somewhere else but I always suggest to add a custom prefix to all the elements. Here it is easy to mistake custom CartTotalsModule with the core one.

```

### Extending pageMetaResolvers and normalizers

Extending Page Meta Resolvers or Normalizer looks the same as with adapters, the only difference is providing them.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be worthwhile to mention why we need to add it as multi-provider and how resolvers are being selected (getScore() method) - this is usually difficult to understand for the newcomers.


```
@NgModule({
imports: [
CommonModule
],
providers: [
{
provide: PageMetaResolver,
useExisting: CustomCartPageMetaResolver,
multi: true,
},
{
provide: PRODUCT_NORMALIZER,
useClass: CustomProductNormalizer,
multi: true,
},
]
})
export class CartTotalModule { }
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
title: I18n Best Practices
---

Spartacus project has default translations. It is obvious that in each project we will need to add our own translations.

The best place to do it is to create file `i18n.ts` in `src/app/spartacus/configurations` folder and export it as `I18nConfig`. We sincerely recommend tp add only global translations here and to add all the rest related to individual modules translations in those modules (describe in modules-best-practices).

For global translations use create `i18n-assets` inside `assets` and than create seperate folder for each language, example: `en`, `de`, etc.

`i18n.ts` example:

```
export const customi18nConfig: I18nConfig = {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that customI18nConfig is more readable

i18n: {
backend: {
loadPath: 'assets/i18n-assets/{{lng}}/{{ns}}.json',
},
chunks: {
common: ['custom_common_chunk',],
errors: ['custom_error_chunk'],
},
fallbackLang: 'en',
},
}),
```

Once we will create the file and provide our own customization we have to provide it in `src/app/spartacus/spartacus-configuration.module.ts`:

```
@NgModule({
...
providers: [
provideConfig(customi18nConfig),
...
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
title: Layout Best Practices
---

## Layout config

By design, each Spartacus project is based on CMS components from Back Office. Spartacus has default `layoutConfig` but it is based on `SpartacusSampleData` and it is unlikely to fit in any other design.

That is why we can override the default Spartacus layout config with our own one.

The best place to do it is to create file `layout.ts` in `src/app/spartacus/configurations` folder and export it as `LayoutConfig`. We can put it here only differences with Sparactus config or provide our custom whole layout config.

Another very common case is adding additional layouts to the configuration and assigning them slots, example:

```export const customLayoutConfig: LayoutConfig = {
layoutSlots: {
prologue: {
xl: {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if you want to add this but I personally don't recommend using breakpoints in the layout config, because it affects performance and causes flickering in SSR. Responsiveness should be achieved via CSS whenever it's possible.

slots: [
'PreHeader',
'SearchBox',
'SiteLogo',
],
},
lg: {
slots: ['PreHeader'', 'SearchBox'],
},
slots: ['PreHeader', 'SiteLogo'],
},
}
};

Once we will create the file and provide our own customization we have to provide it in `src/app/spartacus/spartacus-configuration.module.ts`:

```
@NgModule({
...
providers: [
provideConfig(customLayoutConfig),
...

```

## Storefront layout

In many projects the cx-storefront directive usage will be sufficient, but it is highly probable that to facilitate the work on spartacus it will be easier for us to slightly modify the default storefront layout.

In this case we will recommend to:
1. Create module with component `<our-own-name>-storefront` in `src/app/<our-own-name>-storefront` folder.
2. Extend our own `<our-own-name>StorefrontComponent` with Spartacus `StorefrontComponent`.
3. Copy Spartacus Storefron Component template and paste it in our own Storefront template with our own customization. We can here:
- modify elements order
- add custom elements
4. Import in our Storefront Module all required modules

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason "our Storefront Module" sounds strange to me. Maybe because you're writing this as a core team member and in this case, "our" may refer to the core code ;).
I would write either:

  • your Storefront Module, or
  • our custom Storefront Module

5. Import our Storefront module in `AppModule`

Example custom storefront template:

```
<ng-template [cxOutlet]="StorefrontOutlets.STOREFRONT" cxPageTemplateStyle>
<cx-page-layout section="prologue"></cx-page-layout>

<ng-template cxOutlet="cx-header">
<header
cxSkipLink="cx-header"
[cxFocus]="{ disableMouseFocus: true }"
[class.is-expanded]="isExpanded$ | async"
(keydown.escape)="collapseMenu()"
(click)="collapseMenuIfClickOutside($event)"
>
<cx-page-layout section="header"></cx-page-layout>
<cx-page-layout section="navigation"></cx-page-layout>
</header>
<cx-page-slot position="BottomHeaderSlot"></cx-page-slot>
<cx-global-message
aria-atomic="true"
aria-live="assertive"
></cx-global-message>
</ng-template>

<main cxSkipLink="cx-main" [cxFocus]="{ disableMouseFocus: true }">
<router-outlet></router-outlet>
</main>

<ng-template cxOutlet="cx-footer">
<footer cxSkipLink="cx-footer" [cxFocus]="{ disableMouseFocus: true }">
<cx-page-layout section="footer"></cx-page-layout>
</footer>
</ng-template>

<cx-page-layout section="bottom-info"></cx-page-layout>
</ng-template>

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
---
title: Structure Best Practices
---

Spartacus schematics installation gives us the basic structure of folder structure. It is adding `spartacus` folder with required modules and `feature` folder with feature libs and their modules. We are recommending to not modify `spartacus` folder with any own folder/files because of schematics installation and possible conflicts in the future. The only once modification we can recommend is about `configuration` folder describe in `layout-best-practices.md` and `i18n-best-practices.md`.

## Our structure recommandations:

### Shared folder

Under `app`, create `shared` folder. This folder will contain all elements used globally in the project like cms-components, components, adapters, connectors, guards, configs, directives, pipes, etc.

We will suggest creating a separate folder for each of the above examples. We will recommend also dividing the folder for components into: `components` and `cms-components`, the first one is to keep shared components, but the second one is to keep all shared components with cms mapping.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this will undergo proofreading but using "will" sounds bad to me here. It sounds like you don't suggest/recommend something yet. I think it should be:

  • "we suggest"
  • "we recommend"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure whether cms components should go into a shared folder. They are usually feature specific and they are used only once in the source code - in the mapping configuration. In my opinion putting them into shared folder might result in accidentally importing the mapping multiple times.

Copy link
Contributor Author

@dydome dydome Apr 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mateuszo It is only for shared elements. So if somebody has for example cms-component which is used under the cart, it will be under the cart feature module folder. But if it is banner-component we can put it in shared/cms-components/banner...

Sounds better for you?


### Features folder

Next folder we will recommand to create is `features` folder next to the `shared`. This folder will containt all page main features/modules. For each page funcionality/module we should create seperate folder, including following folders:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

- components
- services
- adapters
- configs
- assets
- ...

Below there is an example of the structure:

<img src="{{ site.baseurl }}/assets/images/structure-best-practises-1.png" alt="Shopping Cart" width="700" border="1px" />

### Features module

For each feature module, we are recommending to provide all configs in main feature module:

```
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CartTotalModule } from './components/cart-total/cart-total.module';
import {
cartCmsConfig,
cartOccConfig,
cartRoutingConfig,
cartTranslationsConfig,
} from './configs/cart.config';
import { provideConfig } from '@spartacus/core';

@NgModule({
imports: [CommonModule, CartTotalModule],
providers: [
provideConfig(cartCmsConfig),
provideConfig(cartOccConfig),
provideConfig(cartRoutingConfig),
provideConfig(cartTranslationsConfig),
],
})
export class CustomCartModule {}
```

with config file `cart.config.ts` in `src/app/features/cart/configs/cart.config.ts`:

```
import {
CmsConfig,
I18nConfig,
OccConfig,
RoutingConfig,
} from '@spartacus/core';

export const cartRoutingConfig: RoutingConfig = {
routing: {
routes: {
customRoute: {
paths: ['cart/custom'],
},
},
},
};

export const cartOccConfig: OccConfig = {
backend: {
occ: {
endpoints: {
cart: 'users/${userId}/carts/${cartId}?fields=Full',
},
},
},
};

export const cartCmsConfig: CmsConfig = {
cmsComponents: {
CartTotalsComponent: {
component: CustomCartTotalsComponent,
},
},
};

const cartTranslationsOverwrites = {
en: {
cart: {
custom: 'Custom',
},
},
};

export const cartTranslationsConfig: I18nConfig = {
i18n: {
resources: cartTranslationsOverwrites,
},
};
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: Styles Best Practices
---

TODO
Binary file added assets/.DS_Store
Binary file not shown.
Binary file added assets/images/.DS_Store
Binary file not shown.
Binary file added assets/images/structure-best-practises-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.