├── .README.md.swp
├── .browserslistrc
├── .editorconfig
├── .gitignore
├── README.md
├── angular.json
├── api
└── products
│ └── [id].ts
├── e2e
├── protractor.conf.js
├── src
│ ├── app.e2e-spec.ts
│ └── app.po.ts
└── tsconfig.json
├── karma.conf.js
├── package-lock.json
├── package.json
├── src
├── app
│ ├── app-routing.module.ts
│ ├── app.component.html
│ ├── app.component.scss
│ ├── app.component.spec.ts
│ ├── app.component.ts
│ ├── app.module.ts
│ ├── core
│ │ ├── flavor.ts
│ │ ├── product.ts
│ │ ├── selectedProductAttributes.ts
│ │ └── size.ts
│ ├── homepage
│ │ ├── homepage.component.html
│ │ ├── homepage.component.scss
│ │ ├── homepage.component.spec.ts
│ │ └── homepage.component.ts
│ ├── mock-products.ts
│ ├── navbar
│ │ ├── navbar.component.html
│ │ ├── navbar.component.scss
│ │ ├── navbar.component.spec.ts
│ │ └── navbar.component.ts
│ ├── product-page
│ │ ├── product-page.component.html
│ │ ├── product-page.component.scss
│ │ ├── product-page.component.spec.ts
│ │ └── product-page.component.ts
│ ├── product.service.spec.ts
│ ├── product.service.ts
│ ├── product
│ │ ├── product.component.html
│ │ ├── product.component.scss
│ │ ├── product.component.spec.ts
│ │ └── product.component.ts
│ └── products
│ │ ├── products.component.html
│ │ ├── products.component.scss
│ │ ├── products.component.spec.ts
│ │ └── products.component.ts
├── assets
│ ├── .gitkeep
│ ├── ice-cream-cherry.svg
│ ├── ice-cream-prune.svg
│ ├── ice-cream-squash.svg
│ ├── popsicle-cherry.svg
│ ├── popsicle-lettuce.svg
│ └── popsicle-lime.svg
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── favicon.ico
├── index.html
├── main.ts
├── polyfills.ts
├── styles.scss
└── test.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.spec.json
└── tslint.json
/.README.md.swp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/snipcart/snipcart-angular-tutorial/a49440b61ac32aa4d1f04993c6587733b23043dc/.README.md.swp
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 |
5 | # For the full list of supported browsers by the Angular framework, please see:
6 | # https://angular.io/guide/browser-support
7 |
8 | # You can see what browsers were selected by your queries by running:
9 | # npx browserslist
10 |
11 | last 1 Chrome version
12 | last 1 Firefox version
13 | last 2 Edge major versions
14 | last 2 Safari major versions
15 | last 2 iOS major versions
16 | Firefox ESR
17 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
18 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.ts]
12 | quote_type = single
13 |
14 | [*.md]
15 | max_line_length = off
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | # Only exists if Bazel was run
8 | /bazel-out
9 |
10 | # dependencies
11 | /node_modules
12 |
13 | # profiling files
14 | chrome-profiler-events*.json
15 | speed-measure-plugin*.json
16 |
17 | # IDEs and editors
18 | /.idea
19 | .project
20 | .classpath
21 | .c9/
22 | *.launch
23 | .settings/
24 | *.sublime-workspace
25 |
26 | # IDE - VSCode
27 | .vscode/*
28 | !.vscode/settings.json
29 | !.vscode/tasks.json
30 | !.vscode/launch.json
31 | !.vscode/extensions.json
32 | .history/*
33 |
34 | # misc
35 | /.sass-cache
36 | /connect.lock
37 | /coverage
38 | /libpeerconnection.log
39 | npm-debug.log
40 | yarn-error.log
41 | testem.log
42 | /typings
43 |
44 | # System Files
45 | .DS_Store
46 | Thumbs.db
47 |
48 | .vercel
49 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Snipcart Angular Tutorial
2 | Because summer is coming in Canada and its been quite a long year, I wanted to replenish my youth memories and make an **REPLENISHING ICE CREAM STORE**!
3 |
4 | ## Angular setup
5 |
6 | Let's first install the Angular CLI. It's a neat tool Angular provides that automates many development tasks. To install it, open a terminal and type the following command:
7 |
8 | `npm install -g @angular/cli`
9 |
10 | Once installed, create a new project with the following command:
11 | `ng new snipcart-angular`
12 |
13 | A prompt will appear asking you if you want to enable strict mode. Select yes. This will enable a few other settings that will help to catch bugs ahead of time.
14 |
15 | In your directory you now see your project repo. Type the following commands to go in it:
16 | ```
17 | cd snipcart-angular
18 | ng serve --open
19 | ```
20 | ng serve will build the app, while the --open option will open up a browser to `http://localhost:4200/`. You should now see Angular generic template page.
21 |
22 | ### Customizing the html template
23 | Now that the general project setup is done. Let's start customizing it!
24 |
25 | First of all, open the `app.component.ts` file and change the value of the title property for 'Ice cream store'.
26 |
27 | Afterwards, open the `app.component.html` file and remove all of the template section, replacing it by `
{{title}}
`. The final result should look like this:
28 |
29 | ```html
30 |
{{title}}
31 |
32 | ```
33 |
34 | Afterwards, the browser should reload instantly, displaying only the new title.
35 |
36 | #### Customizing the stylesheet
37 |
38 | For help with our styling, we will use the angular material components. These components made by the material team are an implementation of [material design](https://material.io/). They will allow us to quickly create a fast, design tested e-commerce.
39 |
40 | Let's use the following command to install material UI: `ng add @angular/material`
41 |
42 |
43 | You can then replace `src/app.component.scss`.
44 |
45 | ### Creating mock products
46 | Later in the tutorial, we will use a full InMemoryDbService. For now, let's do something simple and simply create a `mock-products.ts` file in our root component:
47 | ```ts
48 | import { Product } from './core/product';
49 | import { Size } from './core/size';
50 |
51 | export const PRODUCTS: Product[] = [
52 | {
53 | id: 1,
54 | name: 'Ice Cream',
55 | imageUrls: ['../assets/ice-cream-prune.svg', '../assets/ice-cream-cherry.svg', '../assets/ice-cream-squash.svg'],
56 | price: 10,
57 | flavors: [
58 | { name: 'prune', color: '#5A188E' },
59 | { name: 'squash', color: '#F88532' },
60 | { name: 'cherry', color: '#E91E63' },
61 | ],
62 | sizes: [Size.SMALL, Size.MEDIUM, Size.LARGE],
63 | },
64 | {
65 | id: 2,
66 | name: 'Popsicle',
67 | imageUrls: ['../assets/popsicle-lime.svg', '../assets/popsicle-lettuce.svg', '../assets/popsicle-cherry.svg'],
68 | price: 8,
69 | flavors: [
70 | { name: 'lime', color: '#00CACA' },
71 | { name: 'lettuce', color: '#80DC0B' },
72 | { name: 'cherry', color: '#E91E63' },
73 | ],
74 | sizes: [Size.SMALL, Size.LARGE],
75 | },
76 | ];
77 | ```
78 |
79 | You will also need to create a `core` folder that will contain our typescript product interface as well as a flavor and size interface we will use to better fulfill our customer's wishes!
80 |
81 | ```ts
82 | // core/product.ts
83 |
84 | import { Flavor } from "./flavor";
85 | import { Size } from "./size";
86 |
87 | export interface Product {
88 | id: number;
89 | name: string;
90 | imageUrls: string[];
91 | price: number;
92 | flavors: Flavor[];
93 | sizes: Size[];
94 | }
95 | ```
96 |
97 | ```ts
98 | // core/flavor.ts
99 | export interface Flavor {
100 | name: string;
101 | color: string;
102 | }
103 | ```
104 |
105 | ```ts
106 | // core/size.ts
107 | export enum Size {
108 | SMALL = "small",
109 | MEDIUM = "medium",
110 | LARGE = "large",
111 | }
112 | ```
113 | ## Create a homepage component
114 |
115 | Let's now create our website's homepage, where we will display our product header and options.
116 |
117 | In your terminal type the following command: `ng generate component homepage`.
118 |
119 | Then, let's start by adding props to the homepage. It will be used to display our app title to our website, let's create 2 properties for our app title and subtitle.
120 |
121 | ```ts
122 | // homepage.component.ts
123 |
124 | @Component({
125 | selector: 'app-homepage',
126 | templateUrl: './homepage.component.html',
127 | styleUrls: ['./homepage.component.scss']
128 | })
129 | export class HomepageComponent {
130 | title = 'Infinite summer ice cream store';
131 | subtitle = 'Which one do you want?';
132 | }
133 | ```
134 |
135 | Then let's add the html templating in the corresponding file:
136 |
137 | ```html
138 |
139 |
140 |
{{ title }}
141 |
{{ subtitle }}
142 |
143 | ```
144 |
145 | In order to display the homepage view, add the following line to the component:
146 |
147 | ```html
148 |
149 |
150 |
151 |
152 | ```
153 |
154 | You should now see the title and subtitle displayed! But what's a e-commerce homepage header without some product to show?
155 | ## Display products: introducing directives
156 | Now that we have a cool header and some nice products, let's display them on our website! We will do it in a separate component that will improve reusability.
157 |
158 | In your terminal, create the product-display component by typing the following Angular CLI command: `ng generate component products`.
159 |
160 | Now that we have our component, let's first import the products that we need. Angular allows us to do so easily. Simply add the following lines in `product-display-component.ts`:
161 |
162 | ```ts
163 | import { PRODUCTS } from '../mock-products';
164 | ```
165 |
166 | And then define the attributes in the component class with the following lines:
167 |
168 | ```ts
169 | export class ProductsComponent implements OnInit {
170 |
171 | products = PRODUCTS;
172 | }
173 | ```
174 |
175 | We are now ready to create a product component to give further information about them to our customers.
176 |
177 | `ng generate component product`.
178 |
179 | Open the newly created `product.component.ts` file and replace it with the following content:
180 |
181 | ```ts
182 | import { Component, Input, OnInit } from '@angular/core';
183 | import { Product } from '../core/product';
184 |
185 | @Component({
186 | selector: 'app-product',
187 | templateUrl: './product.component.html',
188 | styleUrls: ['./product.component.scss']
189 | })
190 | export class ProductComponent implements OnInit {
191 | @Input() product: Product | undefined;
192 | imageUrl :string = "";
193 |
194 | ngOnInit() {
195 | this.imageUrl = this.product?.imageUrls[0] ?? '';
196 | }
197 | }
198 | ```
199 |
200 | The `@Input` decorator we added allow us to declare *input properties*. This means that the component can now receive its value directly from its parent component.
201 |
202 | Let's look at what we just created: first, an input property product which binds to the Product object we created in core, and secondly a imageUrl property which we will use to display our product's image. With the line `this.imageUrl = this.product?.imageUrls[0] ?? '';` we then assign to imageUrl the value of the first image in the imageUrls array if it exists, and do so within the ngOnInit method.
203 |
204 | ngOnInit is an Angular lifecycle method that gets called after component initialization. Since the component is initialized, the input props, in our case the product prop, is populated and we can access it. This is what allows us to populate the property.
205 |
206 | Now that our component properties are defined, we can add them to our html (`product.component.html`):
207 |
208 | ```html
209 |
210 |
211 |
212 | ```
213 |
214 | Notice the `*ngFor` directive we are using. It allows us to modify the DOM structure by - you guessed it - looping over the elements of the `products` list and creating the specified html node for it (in our case, the app-products node).
215 |
216 | [Directives] such as `*ngFor` are defined in [Angular's official documentation](https://angular.io/guide/built-in-
217 | directives#built-in-structural-directives) as "classes that add additional behavior to elements". In that sense, components are also directives, because they define additional behavior to a standard html template.
218 |
219 | ### Create a product page: introducing angular routing
220 |
221 | Open `app-routing.module.ts` and insert the following into routes:
222 |
223 | ```ts
224 | const routes: Routes = [
225 | {path: "**", component: HomepageComponent},
226 | ];
227 | ```
228 |
229 | We can now add a dynamic route.
230 |
231 | ```ts
232 | const routes: Routes = [
233 | {path: "product/:id", component: ProductPageComponent},
234 | {path: "**", component: HomepageComponent},
235 | ];
236 | ```
237 | The "**" path is a wildcard route, generally used for 404 pages. It's important to add the wildcard route last, otherwise it will override the other routes.
238 |
239 | In `app.component.html`, `` will display the component related to the route. So in your browser, you can now go to `http://localhost:4200/` and it will point to our homepage!
240 |
241 | ## Create product page component: discovering services
242 |
243 | Components are responsible for data presentation, to improve our application modularity, they shouldn't access the application data directly. Instead, they should interact with **services** which handle data access.
244 |
245 | Let's refactor our `products` component so that it uses a service to handle data access. For now, this service will use mock data.
246 |
247 | In the terminal, enter the following command: `ng generate service product`.
248 |
249 | In te newly created `product.service.ts` fil, add the following content:
250 |
251 | ```ts
252 | import { Injectable } from '@angular/core';
253 | import { PRODUCTS } from './mock-products';
254 |
255 | @Injectable({
256 | providedIn: 'root'
257 | })
258 | export class ProductService {
259 |
260 | constructor() { }
261 |
262 | getProducts(): Product[] {
263 | return PRODUCTS;
264 | }
265 | ```
266 |
267 | The `getProducts` method simply return our mock data. Later in the tutorial we will modify it to make it even more modular. Then, in `products.component.ts` replace the mock products assignation by a call from product service:
268 |
269 | ```ts
270 | export class ProductsComponent implements OnInit {
271 | products: Product[] = [];
272 |
273 | constructor(private productService: ProductService) {}
274 |
275 | getProducts(): void {
276 | this.products = this.productService.getProducts();
277 | }
278 |
279 | ngOnInit() {
280 | this.getProducts();
281 | }
282 | }
283 | ```
284 |
285 | We have done 4 things here: first, we replaced the value of products with an empty array, secondly, we injected the productService in our constructor. Then we defined a getProducts method in our component that handles the products logic. Lastly, we called that method in the ngOnInit lifecycle method.
286 |
287 | Now for our product page, we will need data about a single product, let's add a `getProduct` method to our product service to fetch this data:
288 |
289 | ```ts
290 | // product.service.ts
291 |
292 | getProduct(id: number): Observable {
293 | const product = PRODUCTS.find(product => product.id === id);
294 | return of(product);
295 | }
296 | ```
297 |
298 | This method returns an [observable](https://angular.io/guide/observables) which are used in angular for event handling and, as in our case, asynchronous programming. Later when designing our product page, we will see how to fetch observable values.
299 |
300 | Lt's use this method in our product component to display the product content: `ng generate component product`.
301 |
302 |
303 | ```ts
304 | // product.component.ts
305 | import { Component, Input, OnInit } from '@angular/core';
306 | import { Product } from '../core/product';
307 |
308 | @Component({
309 | selector: 'app-product',
310 | templateUrl: './product.component.html',
311 | styleUrls: ['./product.component.scss']
312 | })
313 | export class ProductComponent implements OnInit {
314 | @Input() product: Product | undefined;
315 | imageUrl :string = "";
316 |
317 | ngOnInit() {
318 | this.imageUrl = this.product?.imageUrls[0] ?? '';
319 | }
320 | }
321 | ```
322 | In the typescript file, we added a product input property for the inserted product, along with an `imageUrl` property, which we bind to the component's image src.
323 |
324 | ```html
325 | // product.component.html
326 |
327 |
{{ product?.name }}
328 |
329 |
330 | ```
331 |
332 | In the html, we also added a router link to a product page, which we have not yet defined. Let's do it now.
333 | ### Add product page
334 |
335 | First, let's allow our users to select the flavor and size variant they want for their products.
336 |
337 | Along with an imageUrl property similar to the one we added to the product component, let's add a `getProduct` method that will get the dynamic parameter from our route and use it to call the corresponding method we defined in product service:
338 |
339 | ```ts
340 | // in product-page.component.ts
341 | imageUrl: string = '';
342 | product: Product | undefined;
343 |
344 | getProduct(): void {
345 | const id = Number(this.route.snapshot.paramMap.get('id'));
346 | this.productService
347 | .getProduct(id)
348 | .subscribe((product) => (this.product = product));
349 | }
350 | ```
351 |
352 | We can see that the method calls our product service `getProduct` method and *subscribes* to the observable value. When the value from getProduct gets returned, it will be assigned to the `product` property.
353 |
354 | Now that we have all of the required data, let's display our product name, image url and price:
355 |
356 | ```html
357 |