diff --git a/libs/cart/README.md b/libs/cart/README.md index f68901680d..45cc5e002b 100644 --- a/libs/cart/README.md +++ b/libs/cart/README.md @@ -1,4 +1,115 @@ # @daffodil/cart - Building and maintaining a model and code for an ecommerce store is complex and mentally taxing. `@daffodil/cart` provides clear interfaces, models, and factories for the frontend of an ecommerce store so that you don't have to. + +## Installation +To install the cart library and its dependencies, use the following commands in the terminal. + +Install with npm: +``` +npm install @daffodil/cart @daffodil/core @ngrx/store @ngrx/effects --save +``` + +Install with yarn: +``` +yarn add @daffodil/cart @daffodil/core @ngrx/store @ngrx/effects +``` + +## Getting started +The cart module includes multiple layers of functionality that build on each other. The models can be used on their own. The driver layers can be used with the models but also allow custom extensions to those models to be passed as generics. A state layer sits on top of the driver layer. Individual drivers can be overridden through driver injection tokens and custom extensions to models can be passed into the state layer's generics. + +The recommended way to use Daffodil is with the state layer. + +- [State guide](/libs/cart/guides/state.md) +- [Drivers guide](/libs/cart/guides/drivers.md) +- [Extension guide](/libs/cart/guides/extension.md) + +## Usage + +### Interacting with platforms +Interacting with platforms through the [cart facade](/libs/cart/guides/state.md#using-the-facade) is the recommended method. + +It is possible to interact with platforms by directly calling the drivers. While this requires more work to integrate into components, it offers greater flexibility. See the [drivers guide](/libs/cart/guides/drivers.md) for more information. + +### Using routing guards +The cart module provides a number of routing guards to prevent access to certain pages until specific data becomes available. + +The following example illustrates using the `DaffShippingAddressGuard` to prevent accessing the shipping method page of checkout until a shipping address has been set. + +```ts +import { + DaffShippingAddressGuard, + DaffCartShippingAddressGuardRedirectUrl +} from '@daffodil/cart'; + +@NgModule({ + imports: [ + ..., + RouterModule.forRoot([ + { + path: 'checkout/shipping', + component: CheckoutShippingComponent, + canActivate: [DaffShippingAddressGuard] + }, + { + path: '', + component: HomepageComponent, + }, + ]) + ], + providers: [ + { + provide: DaffCartShippingAddressGuardRedirectUrl, + useValue: '/' + } + ] +}) +class AppModule {} +``` + +> The `'checkout/shipping'` route's activation was guarded with the `DaffShippingAddressGuard`, ensuring that page cannot be accessed unless the cart has a valid shipping address set. The `DaffCartShippingAddressGuardRedirectUrl` token is used to configure the path to which the user is redirected when and if the activation fails. + +### Providing platform-agnostic payment IDs +The cart facade provides a field (`paymentId$`) for agnostic payment IDs. The IDs must be user-supplied to prevent circular package dependencies. Provide an object for the `DaffCartPaymentMethodIdMap` token. The keys of this object should be cart payment methods and the values should be strings. + +```ts +import { + DaffCartPaymentMethodIdMap, + DaffCartFacade, + DaffCartPaymentMethod +} from '@daffodil/cart'; + +@NgModule({ + ..., + providers: [ + { + provide: DaffCartPaymentMethodIdMap, + useValue: { + authorizenet_accept_js: 'authorizenet', + payflowpro: 'paypal' + } + } + ] +}) +class AppModule {} + +@Component({}) +class CartComponent implements OnInit { + paymentID$: Observable; + + constructor(private cartFacade: DaffCartFacade) {} + + ngOnInit() { + this.paymentID$ = this.cartFacade.paymentId$; + } + + setPayment(info) { + this.cartFacade.dispatch(new DaffCartPaymentUpdate({ + method: 'authorizenet_accept_js', + payment_info: info + })); + } +} +``` + +> When `setPayment` is called, the cart payment method will be updated. After this update is finished, the `this.paymentID$` stream will emit `'authorizenet'`. diff --git a/libs/cart/guides/drivers.md b/libs/cart/guides/drivers.md index 3d4b630bb6..73a9fb7849 100644 --- a/libs/cart/guides/drivers.md +++ b/libs/cart/guides/drivers.md @@ -1,17 +1,16 @@ # Drivers +The cart module can interface with supported platforms through drivers. Choose the driver that corresponds to the platform of choice and follow the linked guide to set it up. -Daffodil can interface with supported platforms through drivers. Choose the driver that corresponds to the platform of choice and follow the linked guide to set it up. +## Supported drivers -## Supported Drivers - -### In-Memory Web API - -The In-Memory driver is for rapid development without the need to set up a magento/shopify/etc backend. It will mock out the management of a cart and operate like a functional backend. It is intended for development and testing purposes; it is not meant to be used in production. +### In-Memory web API +The In-Memory driver is for rapid development without the need to set up a magento/shopify/etc backend. It will mock out the management of a cart and operate like a functional backend. It is intended for development and testing purposes and not meant to be used in production. To set up, import the `DaffCartInMemoryDriverModule` from the `@daffodil/cart/testing` library and the `HttpClientInMemoryWebApiModule` from `angular-in-memory-web-api`. - Include `DaffCartInMemoryDriverModule.forRoot()` and `HttpClientInMemoryWebApiModule` in the imports section of `AppModule`. -```typescript +Include `DaffCartInMemoryDriverModule.forRoot()` and `HttpClientInMemoryWebApiModule` in the imports section of `AppModule`. + +```ts import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'; import { DaffCartInMemoryDriverModule } from '@daffodil/cart/testing'; @@ -24,18 +23,18 @@ import { DaffCartInMemoryDriverModule } from '@daffodil/cart/testing'; export class AppModule {} ``` -Now this `DaffCart` implementation will have access to the In-Memory Driver to use while developing. +Now this `DaffCart` implementation will have access to the In-Memory driver to use while developing. -> It is important to note to only have one `daffodil/cart` driver set up in `AppModule` at a time. To set up a driver configuration to make switching between different backend drivers simple, follow the [advanced setup guide](). +> Note: It is important to only have one `daffodil/cart` driver set up at a time in the `AppModule`. To set up a driver configuration to make switching between different backend drivers simple, follow the [advanced setup guide](). ### Magento - The Magento driver communicates with the Magento backend through the GraphQL API. To set up, import the `DaffCartMagentoDriverModule` from the `@daffodil/cart` library and the `ApolloModule` from `apollo-angular`. - Include `DaffCartMagentoDriverModule.forRoot()` and `ApolloModule` in the imports section of `AppModule`. -```typescript +Include `DaffCartMagentoDriverModule.forRoot()` and `ApolloModule` in the imports section of `AppModule`. + +```ts import { ApolloModule } from 'apollo-angular'; import { DaffCartMagentoDriverModule } from '@daffodil/cart'; @@ -48,19 +47,17 @@ import { DaffCartMagentoDriverModule } from '@daffodil/cart'; export class AppModule {} ``` -Now this `DaffCart` implementation will be able to interact with Magento. - -> It is important to note to only have one `@daffodil/cart` driver set up in `AppModule` at a time. To set up a driver configuration to make switching between different backend drivers simple, follow the [advanced setup guide](). +This `DaffCart` implementation will now be able to interact with Magento. -#### Fragment Introspection +> Note: It is important to only have one `@daffodil/cart` driver set up in `AppModule` at a time. To set up a driver configuration to make switching between different backend drivers simple, follow the [advanced setup guide](). -You should set up fragment introspection with the Magento backend. Refer to the [fragment introspection guide](../../../../tools/builders/guides/fragment-introspection.md) for more info. +#### Fragment introspection +You should set up fragment introspection with the Magento backend. Refer to the [fragment introspection guide](../../../../tools/builders/guides/fragment-introspection.md) for more information. ## Usage - The drivers can be injected into components and invoked directly. The following example shows how to list the items in the cart and add a simple item to the cart. -```typescript +```ts import { DaffCartItemServiceInterface, DaffCartItemDriver, diff --git a/libs/cart/guides/extension.md b/libs/cart/guides/extension.md index 6e4be05005..f1db2db792 100644 --- a/libs/cart/guides/extension.md +++ b/libs/cart/guides/extension.md @@ -1,14 +1,12 @@ # Extension +The cart module provides a number of extension mechanisms so that it can be customized to fit specific needs. -`@daffodil/cart` provides a number of extension mechanisms so that it can be customized to fit specific needs. - -## Custom Drivers - +## Custom drivers If the packaged Daffodil drivers don't satisfy the required use cases, they can be overriden by providing custom drivers. Create a service that implements the interface corresponding to the driver in question. If custom behavior is not needed for all driver methods, unimplemented methods can be delegated to the original driver. The following example demonstrates overriding the `create` method of the `DaffCartDriver` while using Magento. -```typescript +```ts import { DaffCartDriver, DaffMagentoCartService @@ -51,11 +49,10 @@ export class CustomMagentoCartService implements DaffCartServiceInterface { class AppModule {} ``` -## Generic Models - +## Generic models All Daffodil layers can operate on generic extensions of vanilla Daffodil models. Custom models can therefore be used while retaining type safety. The following example illustrates customizing the cart model with the cart facade. -```typescript +```ts import { DaffCartFacade, DaffCart @@ -78,22 +75,21 @@ class CartComponent implements OnInit { } ``` -## Extensible GraphQL Fragments - +## Extensible GraphQL fragments Arbitrary additional fields can be requested on the cart object. Inject a GraphQL document node containing fragments on the platform's cart type to define extra fields. Only drivers that use GraphQL support extensible fragments because fragments are specific to GraphQL. The following cart drivers support extensible fragments: + - Magento ### Magento - Provide the `DAFF_CART_MAGENTO_EXTRA_CART_FRAGMENTS` to query additional fields on a Magento cart query. This applies to all of the driver calls that return a `DaffCart`, which is most of them. The additional fields are present on the untyped `extra_attributes` field. The following example demonstrates providing a GraphQL document using the `graphql-tag` library. -```typescript +```ts import gql from 'graphql-tag'; import { DAFF_CART_MAGENTO_EXTRA_CART_FRAGMENTS, diff --git a/libs/cart/guides/getting-started.md b/libs/cart/guides/getting-started.md deleted file mode 100644 index bf39633118..0000000000 --- a/libs/cart/guides/getting-started.md +++ /dev/null @@ -1,9 +0,0 @@ -# Getting Started - -This overview assumes that an Angular project has already been set up and [cart installation](/libs/cart/guides/installation.md) has been completed. If not, we recommend that be done first. - -Daffodil includes multiple layers of functionality that build on each other. The models can be used on their own. The driver layers can be used with the models but also allow custom extensions to those models to be passed as generics. A state layer sits on top of the driver layer. Individual drivers can be overridden through driver injection tokens and custom extensions to models can be passed into the state layer's generics. - -The recommended way to use Daffodil is with the state layer. - -See [the driver](/libs/cart/guides/drivers.md) and [state](/libs/cart/guides/state.md) guides for information about those layers. See [the extension guide](/libs/cart/guides/extension.md) for information about extending Daffodil. See [the usage guide](/libs/cart/guides/usage.md) for general information about the other parts of `@daffodil/cart`. diff --git a/libs/cart/guides/installation.md b/libs/cart/guides/installation.md deleted file mode 100644 index 615fcd42d7..0000000000 --- a/libs/cart/guides/installation.md +++ /dev/null @@ -1,21 +0,0 @@ -# Installation - -## Installing with npm - -To install the cart library and its dependencies, use the following command in the terminal. - -```bash -npm install @daffodil/cart @daffodil/core @ngrx/store @ngrx/effects --save -``` - -## Installing with yarn - -To install the cart library and its dependencies, use the following command in the terminal. - -```bash -yarn add @daffodil/cart @daffodil/core @ngrx/store @ngrx/effects -``` - -## Post-Install - -Refer to [the getting started guide](/libs/cart/guides/getting-started.md) for next steps. diff --git a/libs/cart/guides/state.md b/libs/cart/guides/state.md index 446f722301..f8b443fd1f 100644 --- a/libs/cart/guides/state.md +++ b/libs/cart/guides/state.md @@ -1,11 +1,7 @@ # State +The cart module provides a fully featured state library to streamline the management of an application's state as well as driver interaction. The facade is an abstraction that provides all the functionality needed for standard use. It is the recommended way to interact with the Daffodil state layer. -`@daffodil/cart` provides a fully featured state library to streamline the management of an application's state as well as driver interaction. - -The facade is an abstraction that provides all the functionality needed for standard use. It is the recommended way to interact with the Daffodil state layer. - -## Setting up `AppModule` - +## Setting up the root module To get started, import the `DaffCartStateModule` in `AppModule`. Next, import `StoreModule.forRoot({})`, which will be relevant later on when using the redux and state management features of the cart module. ```typescript @@ -18,13 +14,12 @@ To get started, import the `DaffCartStateModule` in `AppModule`. Next, import `S export class AppModule {} ``` -## Using the Facade - +## Using the facade The `DaffCartStateModule` provides a `DaffCartFacade` that wraps the complexities of the state library into one place. This facade will handle updating the user's cart and can also be used to build UI with behaviors common to a cart. To inject the facade inside a component, include an instance of `DaffCartFacade` in the component's constructor. -```typescript +```ts @Component({}) export class CartComponent { constructor(public cartFacade: DaffCartFacade) {} @@ -33,13 +28,19 @@ export class CartComponent { Once the `DaffCartFacade` has been set up in the component, it can now be used to manage a user's cart. To perform operations on the cart, pass actions to the `DaffCartFacade#dispatch` method. When carts are created using the `DaffCartCreate` action, Daffodil will save the cart ID in local storage and automatically pass it to the driver layer for future operations. Various cart properties and a list of errors are available on the cart facade as observable streams. -> Note that the storage mechanism can be configured. See [the storage guide](../../core/guides/advanced/storage.md#environment-specific-storage-services) for more details. +> Note: The storage mechanism can be configured. See the [storage guide](../../core/guides/advanced/storage.md#environment-specific-storage-services) for more details. -Additionally, the Daffodil cart facade provides three different loading states for each section of the cart. `mutating$` tracks when an update to a cart property is occurring. `resolving$` tracks when new data is being fetched but no updates are taking place. `loading$` emits `true` when either `mutating$` or `resolving$` is `true`. There is also overall `featureLoading$`, `featureMutating$`, and `featureResolving$` streams to track loading for any section of the cart. These can be used to enhance the application's UI. +Additionally, the Daffodil cart facade provides three different loading states for each section of the cart: + +- `mutating$` tracks when an update to a cart property is occurring. +- `resolving$` tracks when new data is being fetched but no updates are taking place. +- `loading$` emits `true` when either `mutating$` or `resolving$` is `true`. + +There is also overall `featureLoading$`, `featureMutating$`, and `featureResolving$` streams to track loading for any section of the cart. These can be used to enhance the application's UI. The following example illustrates a component used to display and manage a cart's items. -```typescript +```ts import { DaffCartItemAdd, DaffCartItemList, @@ -78,15 +79,14 @@ export class CartItemComponent implements OnInit { > In this example, three observable streams are assigned from `cartFacade`. Then when `addSimpleItem` is called, the `cartFacade`'s `dispatch` function is called with the appropriately formed input. The input data is then sent off to the backend and the three observable streams are updated when a response is received. -## Accessing State with Selectors - +## Accessing state with selectors Accessing state with the facade is recommended but for greater flexibility the redux store's state can be directly accessed with Daffodil's selectors. To properly maintain memoization of selectors that are compatible with generic models, Daffodil selectors are hidden behind an exported getter function. This function returns an object that contains the selectors. The following example also showcases a component used to display and manage a cart's items but without using the facade. -```typescript +```ts import { DaffCartItemAdd, DaffCartItemList, @@ -129,12 +129,10 @@ export class CartItemComponent implements OnInit { } ``` -## Cart Resolution - -This tutorial will walk you through Daffodil's Cart Resolution process which is responsible for resolving a user's cart upon application initialization. This behavior is chiefly managed by the `DaffResolvedCartGuard`. - -### Supported Scenarios +## Cart resolution +This tutorial will walk you through the cart resolution process, which is responsible for resolving a user's cart upon application initialization. This behavior is chiefly managed by the `DaffResolvedCartGuard`. +### Supported scenarios At the moment, the following scenarios are handled by the `DaffResolvedCartGuard`. > For customer cart support, use the [@daffodil/cart-customer](/libs/cart-customer/README.md) package. @@ -146,10 +144,9 @@ At the moment, the following scenarios are handled by the `DaffResolvedCartGuard - Upon a resolution failure, bailing out and navigating somewhere outside the scope of a cart resolution (e.g. your Ecommerce Service's API is down). ### Usage - Assuming that you're already using the `DaffCartStateModule` and have previously selected a [driver](/libs/cart/guides/drivers.md) for `@daffodil/cart`, you can simply add the guard to your route's `canActivate` and the guard will handle the rest. -```typescript +```ts import { Routes, RouterModule } from '@angular/router'; import { HelloComponent } from './hello.component'; import { DaffResolvedCartGuard } from '@daffodil/cart/state'; @@ -176,12 +173,11 @@ export class AppModule {} Upon adding the code, load up the route and take a look at the Network Requests in your browser. You should see at least one request to your ecommerce systems's cart endpoint that attempts resolution. ### Configuration - You can configure the route to which the `DaffResolvedCartGuard` navigates when an error occurs during resolution. See the [configuration guide](/libs/cart/guides/configuration.md) and the `resolution` key of `DaffCartStateConfiguration` for more information. ### Gotchas -#### Guard Ordering +#### Guard ordering The guard's return is observable, and as such, when paired with other guards it won't necessarily complete in the order you expect, be sure to be careful about your guard ordering. For example, Angular provides no guarantee that either of these guards runs before the other. @@ -209,10 +205,9 @@ If you need the guarantee, you can nest the guards. ``` ## Configuration - The `@daffodil/cart/state` package exposes a `forRoot` method on the `DaffCartStateModule` that allows you to pass in a configuration object to configure the behavior of the package. -You can import is like so +You can import it like so: ```ts import { Routes, RouterModule } from '@angular/router'; @@ -240,16 +235,13 @@ export class AppModule {} The only argument to `forRoot` is the configuration object. For more information, see `DaffCartStateConfiguration`. -## Dependency Injectable Reducers - +## Dependency injectable reducers `@daffodil/cart/state` provides mechanisms for consuming applications and libraries to modify state reduction behavior. Injected reducers run after the Daffodil reducers and should take care to not violate the `DaffCartReducersState` interface. ### Purpose - `@daffodil/cart/state` consumers may wish to modify the state for the `daffCart` feature in a way not explicitly provided by `@daffodil/cart/state`. A common example is adding payment info to the cart payment object in response to non-`@daffodil/cart/state` actions. ### Usage - The following example demonstrates modifying the cart's payment info in response to a user-defined action. ```ts diff --git a/libs/cart/guides/testing.md b/libs/cart/guides/testing.md index 6eafec6072..e63ace302c 100644 --- a/libs/cart/guides/testing.md +++ b/libs/cart/guides/testing.md @@ -1,11 +1,11 @@ # Testing +The cart module provides a testing package accessible at `@daffodil/cart/testing`. This package provides model factories, facade mocks, and driver mocks to facilitate unit testing. -`@daffodil/cart` provides a testing package accessible at `@daffodil/cart/testing`. This package provides model factories, facade mocks, and driver mocks to facilitate unit testing. - +## Example The following example demonstrates how to unit test a component using Daffodil model factories and the mock facade with the Jasmine testing framework and the `jasmine-marbles` library. `cart.component.ts` -```typescript +```ts import { DaffCartFacade, DaffCartCreate @@ -28,7 +28,7 @@ class CartComponent implements OnInit { ``` `cart.component.spec.ts` -```typescript +```ts import { DaffCartFacade, DaffCartCreate, diff --git a/libs/cart/guides/usage.md b/libs/cart/guides/usage.md deleted file mode 100644 index 2dfd69bd12..0000000000 --- a/libs/cart/guides/usage.md +++ /dev/null @@ -1,92 +0,0 @@ -# Usage - -## Interacting with Platforms - -Interacting with platforms through the Daffodil facade is the recommended method. See [using the facade](/libs/cart/guides/state.md#using-the-facade). - -It is possible to interact with platforms by directly calling the drivers. While this requires more work to integrate into components, it offers greater flexibility. See [the drivers guide](/libs/cart/guides/drivers.md) for more information. - -## Using Routing Guards - -`@daffodil/cart` provides a number of routing guards to prevent access to certain pages until specific data becomes available. - -The following example illustrates using the `DaffShippingAddressGuard` to prevent accessing the shipping method page of checkout until a shipping address has been set. - -```typescript -import { - DaffShippingAddressGuard, - DaffCartShippingAddressGuardRedirectUrl -} from '@daffodil/cart'; - -@NgModule({ - imports: [ - ..., - RouterModule.forRoot([ - { - path: 'checkout/shipping', - component: CheckoutShippingComponent, - canActivate: [DaffShippingAddressGuard] - }, - { - path: '', - component: HomepageComponent, - }, - ]) - ], - providers: [ - { - provide: DaffCartShippingAddressGuardRedirectUrl, - useValue: '/' - } - ] -}) -class AppModule {} -``` - -> The `'checkout/shipping'` route's activation was guarded with the `DaffShippingAddressGuard`, ensuring that page cannot be accessed unless the cart has a valid shipping address set. The `DaffCartShippingAddressGuardRedirectUrl` token is used to configure the path to which the user is redirected when and if the activation fails. - -## Providing Platform-Agnostic Payment IDs - -The cart facade provides a field (`paymentId$`) for agnostic payment IDs. The IDs must be user-supplied to prevent circular package dependencies. Provide an object for the `DaffCartPaymentMethodIdMap` token. The keys of this object should be cart payment methods and the values should be strings. - -```typescript -import { - DaffCartPaymentMethodIdMap, - DaffCartFacade, - DaffCartPaymentMethod -} from '@daffodil/cart'; - -@NgModule({ - ..., - providers: [ - { - provide: DaffCartPaymentMethodIdMap, - useValue: { - authorizenet_accept_js: 'authorizenet', - payflowpro: 'paypal' - } - } - ] -}) -class AppModule {} - -@Component({}) -class CartComponent implements OnInit { - paymentID$: Observable; - - constructor(private cartFacade: DaffCartFacade) {} - - ngOnInit() { - this.paymentID$ = this.cartFacade.paymentId$; - } - - setPayment(info) { - this.cartFacade.dispatch(new DaffCartPaymentUpdate({ - method: 'authorizenet_accept_js', - payment_info: info - })); - } -} -``` - -> When `setPayment` is called, the cart payment method will be updated. After this update is finished, the `this.paymentID$` stream will emit `'authorizenet'`.