├── .bowerrc ├── .editorconfig ├── .gitignore ├── .jshintrc ├── .travis.yml ├── .yo-rc.json ├── LICENSE ├── README.md ├── bower.json ├── dist ├── angular-contentful.js └── angular-contentful.min.js ├── examples ├── how-to-use-the-contentful-service │ ├── app.js │ └── index.html └── how-to-use-the-directives │ ├── app.js │ └── index.html ├── gulpfile.js ├── karma-dist-concatenated.conf.js ├── karma-dist-minified.conf.js ├── karma-src.conf.js ├── package.json ├── src └── contentful │ ├── contentful.module.js │ ├── controllers │ └── contentful-directive.controller.js │ ├── directives │ ├── contentful-entries.directive.js │ └── contentful-entry.directive.js │ └── services │ ├── contentful-helpers.service.js │ └── contentful.service.js └── test └── unit └── contentful ├── contentful.module.spec.js ├── directives ├── contentful-entries.directive.spec.js └── contentful-entry.directive.spec.js └── services ├── contentful-helpers.service.spec.js └── contentful.service.spec.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower" 3 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ._* 2 | .~lock.* 3 | .buildpath 4 | .DS_Store 5 | .idea 6 | .project 7 | .settings 8 | 9 | # Ignore node stuff 10 | node_modules/ 11 | npm-debug.log 12 | libpeerconnection.log 13 | 14 | # OS-specific 15 | .DS_Store 16 | 17 | # Bower components 18 | bower 19 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "camelcase": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "es3": false, 7 | "forin": true, 8 | "freeze": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": "nofunc", 12 | "newcap": true, 13 | "noarg": true, 14 | "noempty": true, 15 | "nonbsp": true, 16 | "nonew": true, 17 | "plusplus": false, 18 | "quotmark": "single", 19 | "undef": true, 20 | "unused": false, 21 | "strict": false, 22 | "maxparams": 10, 23 | "maxdepth": 5, 24 | "maxstatements": 40, 25 | "maxcomplexity": 8, 26 | "maxlen": 120, 27 | 28 | "asi": false, 29 | "boss": false, 30 | "debug": false, 31 | "eqnull": true, 32 | "esnext": false, 33 | "evil": false, 34 | "expr": false, 35 | "funcscope": false, 36 | "globalstrict": false, 37 | "iterator": false, 38 | "lastsemic": false, 39 | "laxbreak": false, 40 | "laxcomma": false, 41 | "loopfunc": true, 42 | "maxerr": false, 43 | "moz": false, 44 | "multistr": false, 45 | "notypeof": false, 46 | "proto": false, 47 | "scripturl": false, 48 | "shadow": false, 49 | "sub": true, 50 | "supernew": false, 51 | "validthis": false, 52 | "noyield": false, 53 | 54 | "browser": true, 55 | "node": true, 56 | 57 | "globals": { 58 | "angular": false, 59 | "$": false 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 4.0 4 | before_script: 5 | - npm install -g bower 6 | - bower install 7 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-angularjs-library": { 3 | "props": { 4 | "author": { 5 | "name": "Jurgen Van de Moere", 6 | "email": "jurgen.van.de.moere@gmail.com" 7 | }, 8 | "libraryName": { 9 | "original": "contentful", 10 | "camelized": "contentful", 11 | "dasherized": "contentful", 12 | "slugified": "contentful", 13 | "parts": [ 14 | "contentful" 15 | ] 16 | }, 17 | "includeModuleDirectives": true, 18 | "includeModuleFilters": false, 19 | "includeModuleServices": true, 20 | "includeAngularModuleResource": false, 21 | "includeAngularModuleCookies": false, 22 | "includeAngularModuleSanitize": false, 23 | "librarySrcDirectory": "src/contentful", 24 | "libraryUnitTestDirectory": "test/unit/contentful", 25 | "libraryUnitE2eDirectory": "test/e2e/contentful" 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Jurgen Van de Moere 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AngularJS Contentful 2 | 3 | AngularJS module to easily access the [Contentful](https://www.contentful.com) [content delivery API](https://www.contentful.com/developers/documentation/content-delivery-api/): 4 | 5 | ![angular-contentful](https://cloud.githubusercontent.com/assets/1859381/10281579/b8b29e38-6b75-11e5-8975-7732c9533487.png) 6 | 7 | [![Build Status](https://travis-ci.org/jvandemo/angular-contentful.svg?branch=master)](https://travis-ci.org/jvandemo/angular-contentful) 8 | 9 | - lightweight (< 3KB minified) 10 | - no external dependencies 11 | - automatically resolves linked content in the response for maximum convenience 12 | - uses the native AngularJS `$http` service to connect to the API 13 | - returns native AngularJS `$q` promises 14 | 15 | ## Demo 16 | 17 | There are working demo's available in the [examples](examples) directory. 18 | 19 | ## Usage 20 | 21 | First install the module using bower: 22 | 23 | ```bash 24 | $ bower install angular-contentful 25 | ``` 26 | 27 | or npm: 28 | 29 | ```bash 30 | $ npm install angular-contentful 31 | ``` 32 | 33 | and add the library to your application: 34 | 35 | ```xml 36 | 37 | ``` 38 | 39 | > The `src` attribute value above is an example when using `bower`. Your local library path may vary depending on whether you used `bower` or `npm` as your installation method and whether or not you have a build process in place. 40 | 41 | Then add the `contentful` module to the dependencies of your AngularJS application module: 42 | 43 | ```javascript 44 | angular.module('yourApp', ['contentful']); 45 | ``` 46 | 47 | and configure the `contentful` service in a config block using the provider: 48 | 49 | ```javascript 50 | angular 51 | .module('yourApp') 52 | .config(function(contentfulProvider){ 53 | contentfulProvider.setOptions({ 54 | space: 'yourSpace', 55 | accessToken: 'yourAccessToken' 56 | }); 57 | }); 58 | ``` 59 | 60 | Now you can use one of the directives to fetch Contentful data right from within your markup: 61 | 62 | ```xml 63 |
 64 |   {{ $contentfulEntry | json }}
 65 | 
66 | ``` 67 | 68 | or you can use the `contentful` service anywhere in your application code: 69 | 70 | ```javascript 71 | angular 72 | .module('yourApp') 73 | .controller('SomeCtrl', function(contentful){ 74 | 75 | // Get all entries 76 | contentful 77 | .entries() 78 | .then( 79 | 80 | // Success handler 81 | function(response){ 82 | var entries = response.data; 83 | console.log(entries); 84 | }, 85 | 86 | // Error handler 87 | function(response){ 88 | console.log('Oops, error ' + response.status); 89 | } 90 | ); 91 | 92 | }); 93 | ``` 94 | 95 | ## The contentful-entry directive 96 | 97 | Fetches a Contentful entry asynchronously in the background and makes it available in your child markup as `$contentfulEntry` as soon as a response from Contentful is received. 98 | 99 | Requires a Contentful entry id or a query string to be passed. 100 | 101 | #### Fetch an entry by id 102 | 103 | To display an entire entry with id *6KntaYXaHSyIw8M6eo26OK*: 104 | 105 | ```xml 106 | 107 |
108 |   {{ $contentfulEntry | json }}
109 | 
110 | 111 | 112 |
113 |   {{ $contentfulEntry | json }}
114 | 
115 | ``` 116 | 117 | Or to display only one field of the entry: 118 | 119 | ```xml 120 |

121 | Hi {{ $contentfulEntry.fields.name }}! 122 |

123 | ``` 124 | 125 | `$contentfulEntry` is available in the child elements as well: 126 | 127 | ```xml 128 |
129 |
130 | {{ $contentfulEntry.fields.sectionOne }} 131 |
132 |
133 | {{ $contentfulEntry.fields.sectionTwo }} 134 |
135 |
136 | ``` 137 | 138 | To make Contentful resolve the linked content in the entry, use [include](https://www.contentful.com/developers/documentation/content-delivery-api/#search-link): 139 | 140 | ```xml 141 |
142 |   {{ $contentfulEntry | json }}
143 | 
144 | ``` 145 | 146 | to specify the number of levels of linked entries to resolve. 147 | 148 | #### Fetch an entry by query string 149 | 150 | Often you want to fetch an entry by a property other than `sys.id`. 151 | 152 | Therefore the directive also allows you to specify a query string instead of an id like this: 153 | 154 | ```xml 155 |

156 | Hi {{ $contentfulEntry.fields.name }}! 157 |

158 | ``` 159 | 160 | **Notice** 161 | 162 | Behind the scenes all entries matching your query will be fetched and the first item will be assigned to `$contentfulEntry`. 163 | 164 | To reduce data traffic it is highly recommended to use a query string that results in only one entry or add a `limit=1` statement to your query like this: 165 | 166 | ```xml 167 |

168 | Hi {{ $contentfulEntry.fields.name }}! 169 |

170 | ``` 171 | 172 | ## The contentful-entries directive 173 | 174 | Fetches multiple Contentful entries asynchronously in the background and makes them available in your child markup as `$contentfulEntries` as soon as a response from Contentful is received. 175 | 176 | Takes an optional query string value to pass to the Contentful content delivery API. 177 | 178 | For example, to fetch all entries in your space: 179 | 180 | ```xml 181 | 186 | ``` 187 | 188 | Or specify a query string to filter the entries: 189 | 190 | ```xml 191 | 192 | 197 | 198 | 199 | 204 | ``` 205 | 206 | The optional query string is passed to the Contentful API, so you can use all [supported filters](https://www.contentful.com/developers/documentation/content-management-api/#search-filter). 207 | 208 | Links are automatically resolved too, so you can easily access linked content as embedded data like this: 209 | 210 | ```xml 211 | 217 | ``` 218 | 219 | ## The contentful service 220 | 221 | The `contentful` service can be injected anywhere in your application and exposes the following API: 222 | 223 | ### contentful.asset(id, optionSet) 224 | 225 | Get an asset. 226 | 227 | ##### Arguments 228 | 229 | - **id** - {string} - Asset id, required 230 | - **optionSet** - {string} - optional - Contentful space options key passed to `setOptions` method of provider. Defaults to `default` key, or if only one space settings are passed it will use that one. 231 | 232 | ###### Example 233 | 234 | ```javascript 235 | // pass key defined in setOptions method of provider 236 | contentful.asset(id, 'another'); 237 | // or you can pass 'default', although it will implicitly use this key 238 | contentful.asset(id, 'default'); 239 | ``` 240 | 241 | ##### Returns 242 | 243 | Promise. 244 | 245 | ### contentful.assets(queryString, optionSet) 246 | 247 | Get assets. 248 | 249 | ##### Arguments 250 | 251 | - **queryString** - {string} - Query string to pass to API, optional 252 | - **optionSet** - {string} - optional - Contentful space options key passed to `setOptions` method of provider. Defaults to `default` key, or if only one space settings are passed it will use that one. 253 | 254 | ###### Example 255 | 256 | ```javascript 257 | // pass key defined in setOptions method of provider 258 | contentful.assets(queryString, 'another'); 259 | // or you can pass 'default', although it will implicitly use this key 260 | contentful.assets(queryString, 'default'); 261 | ``` 262 | 263 | ##### Returns 264 | 265 | Promise. 266 | 267 | ### contentful.contentType(id, optionSet) 268 | 269 | Get a content type. 270 | 271 | ##### Arguments 272 | 273 | - **id** - {string} - Content type id, required 274 | - **optionSet** - {string} - optional - Contentful space options (one of keys passed to setOptions method of provider). If not passed it will use `default` key, or if only one space settings are passed it will use that one. 275 | 276 | ###### Example 277 | 278 | ```javascript 279 | // pass key defined in setOptions method of provider 280 | contentful.contentType(id, 'another'); 281 | // or you can pass 'default', although it will implicitly use this key 282 | contentful.contentType(id, 'default'); 283 | ``` 284 | 285 | ##### Returns 286 | 287 | Promise. 288 | 289 | ### contentful.contentTypes(queryString, optionSet) 290 | 291 | Get content types. 292 | 293 | ##### Arguments 294 | 295 | - **queryString** - {string} - Query string to pass to API, optional 296 | - **optionSet** - {string} - optional - Contentful space options key passed to `setOptions` method of provider. Defaults to `default` key, or if only one space settings are passed it will use that one. 297 | 298 | ###### Example 299 | 300 | ```javascript 301 | // pass key defined in setOptions method of provider 302 | contentful.contentTypes(queryString, 'another'); 303 | // or you can pass 'default', although it will implicitly use this key if parameter is omitted 304 | contentful.contentTypes(queryString, 'default'); 305 | ``` 306 | 307 | ##### Returns 308 | 309 | Promise. 310 | 311 | ### contentful.entry(id, optionSet) 312 | 313 | Get an entry. 314 | 315 | ##### Arguments 316 | 317 | - **id** - {string} - Entry id, required 318 | - **optionSet** - {string} - optional - Contentful space options key passed to `setOptions` method of provider. Defaults to `default` key, or if only one space settings are passed it will use that one. 319 | 320 | ###### Example 321 | 322 | ```javascript 323 | // pass key defined in setOptions method of provider 324 | contentful.entry(id, 'another'); 325 | // or you can pass 'default', although it will implicitly use this key if parameter is omitted 326 | contentful.entry(id, 'default'); 327 | ``` 328 | 329 | ##### Returns 330 | 331 | Promise. 332 | 333 | ### contentful.entries(queryString, optionSet) 334 | 335 | Get entries. 336 | 337 | ##### Arguments 338 | 339 | - **queryString** - {string} - Query string to pass to API, optional 340 | - **optionSet** - {string} - optional - Contentful space options key passed to `setOptions` method of provider. Defaults to `default` key, or if only one space settings are passed it will use that one. 341 | 342 | ###### Example 343 | 344 | ```javascript 345 | // pass key defined in setOptions method of provider 346 | contentful.entries(queryString, 'another'); 347 | // or you can pass 'default', although it will implicitly use this key if parameter is omitted 348 | contentful.entries(queryString, 'default'); 349 | ``` 350 | 351 | ##### Returns 352 | 353 | Promise. 354 | 355 | ### contentful.space(optionSet) 356 | 357 | Get space. 358 | 359 | ##### Arguments 360 | 361 | - **optionSet** - {string} - optional - Contentful space options key passed to `setOptions` method of provider. Defaults to `default` key, or if only one space settings are passed it will use that one. 362 | 363 | ###### Example 364 | 365 | ```javascript 366 | // pass key defined in setOptions method of provider 367 | contentful.space('another'); 368 | // or you can pass 'default', although it will implicitly use this key if parameter is omitted 369 | contentful.space('default'); 370 | ``` 371 | 372 | ##### Returns 373 | 374 | Promise. 375 | 376 | ## Promises 377 | 378 | All methods return a promise. 379 | 380 | Depending on the reponse of the API, either the success handler or the error handler 381 | is called with a destructured representation of the response with the following properties: 382 | 383 | - **data** – {object} – The response body transformed with the transform functions. 384 | - **status** – {number} – HTTP status code of the response. 385 | - **headers** – {function([headerName])} – Header getter function. 386 | - **config** – {object} – The configuration object that was used to generate the request. 387 | - **statusText** – {string} – HTTP status text of the response. 388 | 389 | The data property contains the Contentful data. The other properties are passed for convenience in case you need them. 390 | 391 | ```javascript 392 | contentful 393 | .entries() 394 | .then( 395 | 396 | // Success handler 397 | function(response){ 398 | var entries = response.data; 399 | console.log(entries); 400 | }, 401 | 402 | // Error handler 403 | function(response){ 404 | console.log('Oops, error ' + response.status); 405 | } 406 | ); 407 | ``` 408 | 409 | ## Automatically resolved linked content 410 | 411 | Angular-contentful automatically resolves linked content for you. 412 | 413 | If the Contentful API response includes linked content such as linked entries or linked assets, they are 414 | automatically attached to their parent content for maximum convenience. 415 | 416 | Suppose you have a collection of dogs that have an image linked to them, you can now access the image 417 | as a direct property instead of having to resolve the image manually: 418 | 419 | ```xml 420 | 426 | ``` 427 | 428 | Due to how the Contentful API works, linked content is only available when using `contentful-entries`, not when using `contentful-entry`. [Read more details here](https://github.com/jvandemo/angular-contentful/issues/11#issuecomment-140298861). 429 | 430 | #### Notice 431 | 432 | Resolving links hierarchically can cause circular links. 433 | 434 | Although this isn't harmful, it may hamper you from outputting the entire response e.g. using `{{ $contentfulEntries | json }}`. 435 | 436 | ## Connecting to multiple spaces 437 | 438 | If you need to connect to more than one Contentful space, you can specify additional spaces in the `contentfulProvider` configuration: 439 | 440 | ```javascript 441 | angular 442 | .module('yourApp') 443 | .config(function(contentfulProvider){ 444 | contentfulProvider.setOptions({ 445 | 'default': { 446 | host: 'cdn.contentful.com', 447 | space: 'first_space', 448 | accessToken: 'first_token' 449 | }, 450 | 'another': { 451 | host: 'cdn.contentful.com', 452 | space: 'second_space', 453 | accessToken: 'second_token' 454 | } 455 | ... 456 | }); 457 | }); 458 | ``` 459 | 460 | and pass in the space key as the `optionSet` argument when calling a contentful service method: 461 | 462 | ```javascript 463 | angular 464 | .module('yourApp') 465 | .controller('SomeCtrl', function(contentful){ 466 | 467 | // Get all entries 468 | contentful 469 | .entries('', 'another') 470 | .then( 471 | 472 | // Success handler 473 | function(response){ 474 | var entries = response.data; 475 | console.log(entries); 476 | }, 477 | 478 | // Error handler 479 | function(response){ 480 | console.log('Oops, error ' + response.status); 481 | } 482 | ); 483 | 484 | }); 485 | ``` 486 | 487 | If you initialize `contentfulProvider` with only one set of options, it will be treated as the default one. 488 | 489 | Currently, the directives do not allow you to specify a space and will always connect to the default space. 490 | 491 | ## Example raw Contentful responses 492 | 493 | These raw response examples give you an idea of what original 494 | Contentful responses look like. 495 | 496 | If you are interested in the details, please visit the [Contentful delivery API documentation](https://www.contentful.com/developers/documentation/content-delivery-api/). 497 | 498 | #### Example Contentful response for successful request 499 | 500 | ```javascript 501 | { 502 | "data": { 503 | "sys": { 504 | "type": "Space", 505 | "id": "cfexampleapi" 506 | }, 507 | "name": "Contentful Example API", 508 | "locales": [ 509 | { 510 | "code": "en-US", 511 | "default": true, 512 | "name": "English" 513 | }, 514 | { 515 | "code": "tlh", 516 | "default": false, 517 | "name": "Klingon" 518 | } 519 | ] 520 | }, 521 | "status": 200, 522 | "config": { 523 | "method": "GET", 524 | "transformRequest": [ 525 | null 526 | ], 527 | "transformResponse": [ 528 | null 529 | ], 530 | "headers": { 531 | "Accept": "application/json, text/plain, */*" 532 | }, 533 | "params": { 534 | "access_token": "b4c0n73n7fu1" 535 | }, 536 | "url": "https://cdn.contentful.com:443/spaces/cfexampleapi" 537 | }, 538 | "statusText": "OK" 539 | } 540 | ``` 541 | 542 | #### Example response for error 543 | 544 | ```javascript 545 | { 546 | "data": { 547 | "sys": { 548 | "type": "Error", 549 | "id": "NotFound" 550 | }, 551 | "message": "The resource could not be found.", 552 | "details": { 553 | "sys": { 554 | "type": "Space" 555 | } 556 | }, 557 | "requestId": "71a-1131131513" 558 | }, 559 | "status": 404, 560 | "config": { 561 | "method": "GET", 562 | "transformRequest": [ 563 | null 564 | ], 565 | "transformResponse": [ 566 | null 567 | ], 568 | "headers": { 569 | "Accept": "application/json, text/plain, */*" 570 | }, 571 | "params": { 572 | "access_token": "b4c0n73n7fu1" 573 | }, 574 | "url": "https://cdn.contentful.com:443/spaces/cfexampleapiii" 575 | }, 576 | "statusText": "Not Found" 577 | } 578 | ``` 579 | 580 | ## Why not use the official contentful.js? 581 | 582 | The official Contentful to way is to include 2 libraries in your application: 583 | 584 | ```xml 585 | 586 | 587 | ``` 588 | 589 | #### The problem 590 | 591 | `contentful.js` is the main Contentful JavaScript library that relies on different external libraries such as: 592 | 593 | - `questor` to perform HTTP requests 594 | - `bluebird` for promises 595 | 596 | and then bundles everything together in `contentful.js` using Browserify, resulting in a file that packs over `100KB` minified. 597 | 598 | `ng-contenful.js` then forms a wrapper around `contentful.js` that takes care of converting the `bluebird` promises back to AngularJS promises. 599 | 600 | This makes sense if you are in a non-AngularJS environment such as node.js, but AngularJS already has built-in services to perform HTTP requests and provide promises. 601 | 602 | #### The solution 603 | 604 | This AngularJS module uses native AngularJS services to provide a similar API using: 605 | 606 | - `$http` to perform HTTP requests 607 | - `$q` for promises 608 | 609 | which results in: 610 | 611 | - **NOT** having to include `contentful.js` and `ng-contentful.js`, saving you an expensive 100KB+ client side download when your application loads 612 | - less CPU cycles in the client by not having to convert promises 613 | 614 | ## Contribute 615 | 616 | To update the build in the `dist` directory: 617 | 618 | ```bash 619 | $ gulp 620 | ``` 621 | 622 | To run the unit tests using the src files: 623 | 624 | ```bash 625 | $ gulp test-src 626 | ``` 627 | 628 | To run the unit tests using the unminified library: 629 | 630 | ```bash 631 | $ gulp test-dist-concatenated 632 | ``` 633 | 634 | To run the unit tests using the minified library: 635 | 636 | ```bash 637 | $ gulp test-dist-minified 638 | ``` 639 | 640 | ## Change log 641 | 642 | ### v2.2.0 643 | 644 | - Added support for multiple spaces [#24](https://github.com/jvandemo/angular-contentful/pull/24) (credits to [Vuk Stanković](https://github.com/vuk)) 645 | 646 | ### v2.1.0 647 | 648 | - Added module property to package.json for webpack compatibility 649 | 650 | ### v2.0.0 651 | 652 | - **BREAKING CHANGE**: Added support for specifying expressions in directive attributes 653 | 654 | ### v1.1.0 655 | 656 | - Added query string support to contentful-entry directive 657 | 658 | ### v1.0.0 659 | 660 | - Simplified service API so it always resolves links by default 661 | - Simplified contentful-entries directive API to make data available more intuitively using `$contentfulEntries` instead of `$contentfulEntries.entries` 662 | - Simplified contentful-entry directive API to make data available more intuitively using `$contentfulEntry` instead of `$contentfulEntry.entry` 663 | - Removed support for `success` and `error` shorthand methods in favor of more consistent API with `then`. 664 | - Updated documentation 665 | 666 | ### v0.5.1 667 | 668 | - Update contentful-entries directive so it return response identical to contentful service method 669 | 670 | ### v0.5.0 671 | 672 | - Added support to automatically resolve links when multiple entries are returned 673 | - Updated documentation 674 | 675 | ### v0.4.0 676 | 677 | - Added contentfulEntries directive 678 | - Added additional unit tests 679 | - Updated documentation 680 | 681 | ### v0.3.0 682 | 683 | - Added contentfulEntry directive 684 | - Added additional unit tests 685 | - Updated documentation 686 | 687 | ### v0.2.0 688 | 689 | - Added demo application 690 | - Added shorthand support for `success` and `error` handlers 691 | - Added documentation 692 | 693 | ### v0.1.0 694 | 695 | - Added contentful service 696 | - Added unit tests 697 | - Added initial documentation 698 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-contentful", 3 | "version": "2.2.0", 4 | "authors": [ 5 | { 6 | "name": "Jurgen Van de Moere", 7 | "email": "jurgen.van.de.moere@gmail.com" 8 | } 9 | ], 10 | "keywords": [ 11 | "angular", 12 | "angularjs", 13 | "cms", 14 | "contentful", 15 | "contentful delivery api" 16 | ], 17 | "main": ["dist/angular-contentful.js"], 18 | "ignore": [ 19 | "src", 20 | "test", 21 | "gulpfile.js", 22 | "karma-dist-concatenated.conf.js", 23 | "karma-dist-minified.conf.js", 24 | "karma-src.conf.js", 25 | "**/.*" 26 | ], 27 | "dependencies": {}, 28 | "devDependencies": { 29 | "angular-mocks": ">=1.2.0 <1.5.0", 30 | "angular-scenario": ">=1.2.0 <1.5.0", 31 | "angular": ">=1.2.0 <1.5.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /dist/angular-contentful.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | // Modules 4 | angular.module('contentful', []); 5 | 6 | })(); 7 | 8 | (function () { 9 | 10 | /** 11 | * Controller for contentful directives 12 | * 13 | * By separating the controller we can avoid a lot of 14 | * repetitive code and keep the code base as small as 15 | * possible. 16 | * 17 | * @param $attrs 18 | * @param contentful 19 | * @constructor 20 | */ 21 | function ContentfulDirectiveCtrl($scope, $attrs, contentful, contentfulHelpers) { 22 | 23 | var query; 24 | 25 | // Passed value is required entry id 26 | if ($attrs.contentfulEntry) { 27 | 28 | query = $scope.$eval($attrs.contentfulEntry); 29 | 30 | // In case we detect a query string instead of simple id, we fetch the 31 | // collection and return the first entry 32 | if(contentfulHelpers.isQueryString(query)){ 33 | 34 | // Fetch entry by query 35 | contentful 36 | .entries(query) 37 | .then( 38 | function (response) { 39 | var firstEntry = {}; 40 | if(response.data && response.data.items && response.data.items.length){ 41 | firstEntry = response.data.items[0]; 42 | } 43 | $scope.$contentfulEntry = firstEntry; 44 | }, 45 | function(){ 46 | $scope.$contentfulEntry = {}; 47 | } 48 | ); 49 | 50 | } else { 51 | 52 | // Fetch entry by id 53 | contentful 54 | .entry(query) 55 | .then( 56 | function (response) { 57 | $scope.$contentfulEntry = response.data; 58 | }, 59 | function(){ 60 | $scope.$contentfulEntry = {}; 61 | } 62 | ); 63 | 64 | } 65 | 66 | } 67 | 68 | // Passed value is optional query 69 | if ($attrs.hasOwnProperty('contentfulEntries')) { 70 | 71 | query = $scope.$eval($attrs.contentfulEntries); 72 | 73 | contentful 74 | .entries(query) 75 | .then( 76 | function (response) { 77 | $scope.$contentfulEntries = response.data; 78 | }, 79 | function(){ 80 | $scope.$contentfulEntries = { 81 | limit: 0, 82 | skip: 0, 83 | total: 0, 84 | items: [] 85 | }; 86 | } 87 | ); 88 | } 89 | 90 | } 91 | 92 | // Inject controller dependencies 93 | ContentfulDirectiveCtrl.$inject = ['$scope', '$attrs', 'contentful', 'contentfulHelpers']; 94 | 95 | // Export 96 | angular 97 | .module('contentful') 98 | .controller('ContentfulDirectiveCtrl', ContentfulDirectiveCtrl); 99 | 100 | })(); 101 | 102 | (function () { 103 | 104 | /** 105 | * Directive 106 | * 107 | * @returns {object} directive definition object 108 | */ 109 | function contentfulEntriesDirective() { 110 | 111 | return { 112 | restrict: 'EA', 113 | scope: true, 114 | controller: 'ContentfulDirectiveCtrl' 115 | }; 116 | 117 | } 118 | 119 | // Inject directive dependencies 120 | contentfulEntriesDirective.$inject = []; 121 | 122 | // Export 123 | angular 124 | .module('contentful') 125 | .directive('contentfulEntries', contentfulEntriesDirective); 126 | 127 | })(); 128 | 129 | (function () { 130 | 131 | /** 132 | * Directive 133 | * 134 | * @returns {object} directive definition object 135 | */ 136 | function contentfulEntryDirective() { 137 | 138 | return { 139 | restrict: 'EA', 140 | scope: true, 141 | controller: 'ContentfulDirectiveCtrl' 142 | }; 143 | 144 | } 145 | 146 | // Inject directive dependencies 147 | contentfulEntryDirective.$inject = []; 148 | 149 | // Export 150 | angular 151 | .module('contentful') 152 | .directive('contentfulEntry', contentfulEntryDirective); 153 | 154 | })(); 155 | 156 | (function () { 157 | 158 | function contentfulHelpersFactory() { 159 | 160 | function ContentfulHelpers() { 161 | } 162 | 163 | /** 164 | * Resolve a complete response 165 | * 166 | * @param response 167 | * @returns {Array} 168 | */ 169 | ContentfulHelpers.prototype.resolveResponse = function resolveResponse(response) { 170 | var self = this; 171 | self.walkMutate(response, self.isLink, function (link) { 172 | return self.getLink(response, link) || link; 173 | }); 174 | return response.items || []; 175 | }; 176 | 177 | /** 178 | * Check if object is a link 179 | * 180 | * @param {object} 181 | * @returns {boolean} 182 | */ 183 | ContentfulHelpers.prototype.isLink = function isLink(object) { 184 | if (object && object.sys && object.sys.type && object.sys.type === 'Link') { 185 | return true; 186 | } 187 | return false; 188 | }; 189 | 190 | /** 191 | * Find and return a link in a response 192 | * 193 | * @param response 194 | * @param link 195 | * @returns {object|null} Link 196 | */ 197 | ContentfulHelpers.prototype.getLink = function getLink(response, link) { 198 | var self = this; 199 | var type = link.sys.linkType; 200 | var id = link.sys.id; 201 | var pred = function (resource) { 202 | return resource && resource.sys && resource.sys.type === type && resource.sys.id === id; 203 | }; 204 | return self.findLink(response.items, pred) || 205 | response.includes && self.findLink(response.includes[type], pred); 206 | }; 207 | 208 | /** 209 | * Helper method to find a link in an array 210 | * 211 | * @param {Array} arr - Array to search 212 | * @param {function} pred - Predicate function 213 | * @returns {object|null} Link 214 | */ 215 | ContentfulHelpers.prototype.findLink = function findLink(arr, pred) { 216 | var i; 217 | var link = null; 218 | if (!angular.isArray(arr)) { 219 | return link; 220 | } 221 | for (i = 0; i < arr.length; i++) { 222 | if (pred(arr[i])) { 223 | link = arr[i]; 224 | break; 225 | } 226 | } 227 | return link; 228 | }; 229 | 230 | /** 231 | * Walk a data structure and mutate properties that match the predicate function 232 | * 233 | * @param {object|array} input - Input data 234 | * @param {function} pred - Prediction function 235 | * @param {function} mutator - Mutator function 236 | * @returns {*} 237 | */ 238 | ContentfulHelpers.prototype.walkMutate = function walkMutate(input, pred, mutator) { 239 | var self = this; 240 | if (pred(input)){ 241 | return mutator(input); 242 | } 243 | 244 | if (angular.isArray(input) || angular.isObject(input)) { 245 | angular.forEach(input, function (item, key) { 246 | input[key] = self.walkMutate(item, pred, mutator); 247 | }); 248 | return input; 249 | } 250 | return input; 251 | }; 252 | 253 | 254 | /** 255 | * Check if a string is a query string 256 | * 257 | * @param {string} input 258 | * @returns {boolean} 259 | */ 260 | ContentfulHelpers.prototype.isQueryString = function isQueryString(input) { 261 | if(input.toString().indexOf('=') > -1){ 262 | return true; 263 | } 264 | if(input.toString().indexOf('&') > -1){ 265 | return true; 266 | } 267 | if(input.toString().indexOf('?') > -1){ 268 | return true; 269 | } 270 | return false; 271 | }; 272 | 273 | 274 | 275 | return new ContentfulHelpers(); 276 | 277 | } 278 | 279 | // Export 280 | angular 281 | .module('contentful') 282 | .factory('contentfulHelpers', contentfulHelpersFactory); 283 | 284 | })(); 285 | 286 | (function () { 287 | 288 | /** 289 | * Contentful service provider 290 | */ 291 | function contentfulProvider() { 292 | 293 | // Default options 294 | var options = { 295 | 'default': { 296 | host: 'cdn.contentful.com', 297 | space: null, 298 | accessToken: null, 299 | secure: true 300 | } 301 | }; 302 | 303 | /** 304 | * Set options 305 | * 306 | * @param {object} newOptions 307 | * @returns {contentfulProvider} 308 | */ 309 | this.setOptions = function (newOptions) { 310 | if (newOptions && newOptions.space) { 311 | angular.extend(options['default'], newOptions); 312 | } else { 313 | angular.extend(options, newOptions); 314 | } 315 | return this; 316 | }; 317 | 318 | this.$get = contentfulFactory; 319 | 320 | /** 321 | * Create the contentful service 322 | * 323 | * @returns {contentfulProvider.Contentful} 324 | */ 325 | function contentfulFactory($http, $q, contentfulHelpers) { 326 | return new Contentful($http, $q, contentfulHelpers, options); 327 | } 328 | 329 | // Inject dependencies in factory 330 | contentfulFactory.$inject = ['$http', '$q', 'contentfulHelpers']; 331 | 332 | /** 333 | * Contentful service constructor 334 | * 335 | * @constructor 336 | */ 337 | function Contentful($http, $q, contentfulHelpers, options) { 338 | 339 | this._$http = $http; 340 | this._$q = $q; 341 | this._contentfulHelpers = contentfulHelpers; 342 | this.options = options; 343 | 344 | if (typeof $http.get !== 'function') { 345 | throw new Error('The contentful service needs a valid http service to work with'); 346 | } 347 | 348 | if (typeof $q.when !== 'function') { 349 | throw new Error('The contentful service needs a valid promise service to work with'); 350 | } 351 | } 352 | 353 | /** 354 | * Perform request 355 | * 356 | * @param {string} path 357 | * @param {object} config 358 | * @param {object} optionSet 359 | * @returns {promise} 360 | */ 361 | Contentful.prototype.request = function (path, config, optionSet) { 362 | optionSet = optionSet || 'default'; 363 | var url; 364 | var nonEmptyParams = {}; 365 | 366 | // Make sure config is valid 367 | config = config || {}; 368 | config.headers = config.headers || {}; 369 | config.params = config.params || {}; 370 | 371 | // Add required configuration 372 | config.headers['Content-Type'] = 'application/vnd.contentful.delivery.v1+json'; 373 | config.params['access_token'] = this.options[optionSet].accessToken; 374 | 375 | // Build url 376 | url = [ 377 | this.options[optionSet].secure ? 'https' : 'http', 378 | '://', 379 | this.options[optionSet].host, 380 | ':', 381 | this.options[optionSet].secure ? '443' : '80', 382 | '/spaces/', 383 | this.options[optionSet].space, 384 | path 385 | ].join(''); 386 | 387 | // Perform request and return promise 388 | return this._$http.get(url, config); 389 | }; 390 | 391 | /** 392 | * Get an asset 393 | * 394 | * @param id 395 | * @param optionSet {object} 396 | * @returns {promise} 397 | */ 398 | Contentful.prototype.asset = function (id, optionSet) { 399 | optionSet = optionSet || 'default'; 400 | return this.request('/assets/' + id, {}, optionSet); 401 | }; 402 | 403 | /** 404 | * Get assets 405 | * 406 | * @param query 407 | * @param optionSet {object} 408 | * @returns {promise} 409 | */ 410 | Contentful.prototype.assets = function (querystring, optionSet) { 411 | optionSet = optionSet || 'default'; 412 | return this.processResponseWithMultipleEntries( 413 | this.request('/assets', configifyParams(paramifyQuerystring(querystring)), optionSet) 414 | ); 415 | }; 416 | 417 | /** 418 | * Get content type 419 | * 420 | * @param id 421 | * @param optionSet {object} 422 | * @returns {promise} 423 | */ 424 | Contentful.prototype.contentType = function (id, optionSet) { 425 | optionSet = optionSet || 'default'; 426 | return this.request('/content_types/' + id, {}, optionSet); 427 | }; 428 | 429 | /** 430 | * Get content types 431 | * 432 | * @param query 433 | * @param optionSet {object} 434 | * @returns {promise} 435 | */ 436 | Contentful.prototype.contentTypes = function (querystring, optionSet) { 437 | optionSet = optionSet || 'default'; 438 | return this.processResponseWithMultipleEntries( 439 | this.request('/content_types', configifyParams(paramifyQuerystring(querystring)), optionSet) 440 | ); 441 | }; 442 | 443 | /** 444 | * Get entry 445 | * 446 | * @param id 447 | * @param optionSet {object} 448 | * @returns {promise} 449 | */ 450 | Contentful.prototype.entry = function (id, optionSet) { 451 | optionSet = optionSet || 'default'; 452 | return this.request('/entries/' + id, {}, optionSet); 453 | }; 454 | 455 | /** 456 | * Get entries 457 | * 458 | * @param query 459 | * @param optionSet {object} 460 | * @returns {promise} 461 | */ 462 | Contentful.prototype.entries = function (querystring, optionSet) { 463 | optionSet = optionSet || 'default'; 464 | return this.processResponseWithMultipleEntries( 465 | this.request('/entries', configifyParams(paramifyQuerystring(querystring)), optionSet) 466 | ); 467 | }; 468 | 469 | /** 470 | * Get space 471 | * 472 | * @param optionSet {object} 473 | * @returns {promise} 474 | */ 475 | Contentful.prototype.space = function (optionSet) { 476 | optionSet = optionSet || 'default'; 477 | return this.request('', {}, optionSet); 478 | }; 479 | 480 | /** 481 | * Process multiple incoming entries 482 | * 483 | * Resolves links in the response.data. 484 | * 485 | * @param promise 486 | * @returns {*} 487 | */ 488 | Contentful.prototype.processResponseWithMultipleEntries = function(promise){ 489 | var self = this; 490 | if(promise && promise.then){ 491 | promise.then( 492 | 493 | // Automatically resolve links on success 494 | function(response){ 495 | var entries = { 496 | limit: response.data.limit, 497 | skip: response.data.skip, 498 | total: response.data.total 499 | }; 500 | entries.items = self._contentfulHelpers.resolveResponse(response.data); 501 | response.data = entries; 502 | return response; 503 | }, 504 | 505 | // Forward error on failure 506 | function(response){ 507 | return response; 508 | } 509 | ); 510 | } 511 | return promise; 512 | }; 513 | 514 | /** 515 | * Process single incoming entry 516 | * 517 | * For now, this is just a noop but it exists so it makes 518 | * sure a $q promise is returned with only then method 519 | * (removing shorthand success and error methods) 520 | * and to have single point of entry in case transformation 521 | * is needed in the future. 522 | * 523 | * @param promise 524 | * @returns {*} 525 | */ 526 | Contentful.prototype.processResponseWithSingleEntry = function(promise){ 527 | if(promise && promise.then){ 528 | promise.then( 529 | 530 | // Forward error on failure 531 | function(response){ 532 | return response; 533 | }, 534 | 535 | // Forward error on failure 536 | function(response){ 537 | return response; 538 | } 539 | ); 540 | } 541 | return promise; 542 | }; 543 | 544 | } 545 | 546 | /** 547 | * Create params object from querystring 548 | * 549 | * @param querystring 550 | * @returns {object} params 551 | */ 552 | function paramifyQuerystring(querystring){ 553 | var params = {}; 554 | 555 | if(!querystring){ 556 | return params; 557 | } 558 | 559 | // Split querystring in parts separated by '&' 560 | var couples = querystring.toString().split('&'); 561 | angular.forEach(couples, function(couple){ 562 | 563 | // Split in parts separated by '=' 564 | var parts = couple.split('='); 565 | 566 | // Only add if an actual value is passed 567 | // to prevent empty params in the url 568 | if(parts.length > 1){ 569 | params[parts[0]] = parts[1]; 570 | } 571 | }); 572 | return params; 573 | } 574 | 575 | /** 576 | * Create config object from params 577 | * 578 | * @param params 579 | * @returns {object} config 580 | */ 581 | function configifyParams(params){ 582 | if(!angular.isObject(params)){ 583 | params = {}; 584 | } 585 | return { 586 | params: params 587 | }; 588 | } 589 | 590 | // Export 591 | angular 592 | .module('contentful') 593 | .provider('contentful', contentfulProvider); 594 | 595 | })(); 596 | -------------------------------------------------------------------------------- /dist/angular-contentful.min.js: -------------------------------------------------------------------------------- 1 | !function(){angular.module("contentful",[])}(),function(){function t(t,n,e,r){var i;n.contentfulEntry&&(i=t.$eval(n.contentfulEntry),r.isQueryString(i)?e.entries(i).then(function(n){var e={};n.data&&n.data.items&&n.data.items.length&&(e=n.data.items[0]),t.$contentfulEntry=e},function(){t.$contentfulEntry={}}):e.entry(i).then(function(n){t.$contentfulEntry=n.data},function(){t.$contentfulEntry={}})),n.hasOwnProperty("contentfulEntries")&&(i=t.$eval(n.contentfulEntries),e.entries(i).then(function(n){t.$contentfulEntries=n.data},function(){t.$contentfulEntries={limit:0,skip:0,total:0,items:[]}}))}t.$inject=["$scope","$attrs","contentful","contentfulHelpers"],angular.module("contentful").controller("ContentfulDirectiveCtrl",t)}(),function(){function t(){return{restrict:"EA",scope:!0,controller:"ContentfulDirectiveCtrl"}}t.$inject=[],angular.module("contentful").directive("contentfulEntries",t)}(),function(){function t(){return{restrict:"EA",scope:!0,controller:"ContentfulDirectiveCtrl"}}t.$inject=[],angular.module("contentful").directive("contentfulEntry",t)}(),function(){function t(){function t(){}return t.prototype.resolveResponse=function(t){var n=this;return n.walkMutate(t,n.isLink,function(e){return n.getLink(t,e)||e}),t.items||[]},t.prototype.isLink=function(t){return t&&t.sys&&t.sys.type&&"Link"===t.sys.type?!0:!1},t.prototype.getLink=function(t,n){var e=this,r=n.sys.linkType,i=n.sys.id,o=function(t){return t&&t.sys&&t.sys.type===r&&t.sys.id===i};return e.findLink(t.items,o)||t.includes&&e.findLink(t.includes[r],o)},t.prototype.findLink=function(t,n){var e,r=null;if(!angular.isArray(t))return r;for(e=0;e-1?!0:t.toString().indexOf("?")>-1?!0:!1},new t}angular.module("contentful").factory("contentfulHelpers",t)}(),function(){function t(){function t(t,n,e){return new r(t,n,e,i)}function r(t,n,e,r){if(this._$http=t,this._$q=n,this._contentfulHelpers=e,this.options=r,"function"!=typeof t.get)throw new Error("The contentful service needs a valid http service to work with");if("function"!=typeof n.when)throw new Error("The contentful service needs a valid promise service to work with")}var i={"default":{host:"cdn.contentful.com",space:null,accessToken:null,secure:!0}};this.setOptions=function(t){return t&&t.space?angular.extend(i["default"],t):angular.extend(i,t),this},this.$get=t,t.$inject=["$http","$q","contentfulHelpers"],r.prototype.request=function(t,n,e){e=e||"default";var r;return n=n||{},n.headers=n.headers||{},n.params=n.params||{},n.headers["Content-Type"]="application/vnd.contentful.delivery.v1+json",n.params.access_token=this.options[e].accessToken,r=[this.options[e].secure?"https":"http","://",this.options[e].host,":",this.options[e].secure?"443":"80","/spaces/",this.options[e].space,t].join(""),this._$http.get(r,n)},r.prototype.asset=function(t,n){return n=n||"default",this.request("/assets/"+t,{},n)},r.prototype.assets=function(t,r){return r=r||"default",this.processResponseWithMultipleEntries(this.request("/assets",e(n(t)),r))},r.prototype.contentType=function(t,n){return n=n||"default",this.request("/content_types/"+t,{},n)},r.prototype.contentTypes=function(t,r){return r=r||"default",this.processResponseWithMultipleEntries(this.request("/content_types",e(n(t)),r))},r.prototype.entry=function(t,n){return n=n||"default",this.request("/entries/"+t,{},n)},r.prototype.entries=function(t,r){return r=r||"default",this.processResponseWithMultipleEntries(this.request("/entries",e(n(t)),r))},r.prototype.space=function(t){return t=t||"default",this.request("",{},t)},r.prototype.processResponseWithMultipleEntries=function(t){var n=this;return t&&t.then&&t.then(function(t){var e={limit:t.data.limit,skip:t.data.skip,total:t.data.total};return e.items=n._contentfulHelpers.resolveResponse(t.data),t.data=e,t},function(t){return t}),t},r.prototype.processResponseWithSingleEntry=function(t){return t&&t.then&&t.then(function(t){return t},function(t){return t}),t}}function n(t){var n={};if(!t)return n;var e=t.toString().split("&");return angular.forEach(e,function(t){var e=t.split("=");e.length>1&&(n[e[0]]=e[1])}),n}function e(t){return angular.isObject(t)||(t={}),{params:t}}angular.module("contentful").provider("contentful",t)}(); -------------------------------------------------------------------------------- /examples/how-to-use-the-contentful-service/app.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | // Modules 4 | angular.module('app', ['contentful']); 5 | 6 | angular 7 | .module('app') 8 | .config(function (contentfulProvider) { 9 | contentfulProvider.setOptions({ 10 | space: 'cfexampleapi', 11 | accessToken: 'b4c0n73n7fu1' 12 | }); 13 | }); 14 | 15 | angular 16 | .module('app') 17 | .controller('DemoCtrl', function ($scope, contentful) { 18 | 19 | var promise; 20 | 21 | $scope.busy = false; 22 | $scope.response = null; 23 | 24 | $scope.$watch('action', function (action, old) { 25 | 26 | // Still performing previous request 27 | if ($scope.busy) { 28 | return; 29 | } 30 | 31 | if (action === old) { 32 | return; 33 | } 34 | 35 | promise = null; 36 | $scope.busy = true; 37 | 38 | if (action === 'space') { 39 | promise = contentful.space(); 40 | } 41 | 42 | if (action === 'contentTypes') { 43 | promise = contentful.contentTypes(); 44 | } 45 | 46 | if (action === 'entry') { 47 | promise = contentful.entry('6KntaYXaHSyIw8M6eo26OK'); 48 | } 49 | 50 | if (action === 'entries') { 51 | promise = contentful.entries(); 52 | } 53 | 54 | if (!promise) { 55 | $scope.response = null; 56 | $scope.busy = false; 57 | return; 58 | } 59 | 60 | promise.then( 61 | function (response) { 62 | $scope.response = response; 63 | $scope.busy = false; 64 | }, 65 | function (response) { 66 | $scope.response = response; 67 | $scope.busy = false; 68 | } 69 | ) 70 | 71 | }); 72 | 73 | }); 74 | 75 | })(); 76 | -------------------------------------------------------------------------------- /examples/how-to-use-the-contentful-service/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Simple demo application 4 | 5 | 6 | 7 | 8 | 9 |

Simple demo app

10 |

This simple demo uses the following Contentful configuration:

11 |
    12 |
  • Space: cfexampleapi
  • 13 |
  • Access token: b4c0n73n7fu1
  • 14 |
15 |

Status: IdleBusy

16 |

Actions: 17 | 24 |

25 |

Response

26 |
Please select an action from the dropdown above
27 |
{{response | json}}
28 | 29 | 30 | -------------------------------------------------------------------------------- /examples/how-to-use-the-directives/app.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | // Modules 4 | angular.module('app', ['contentful']); 5 | 6 | angular 7 | .module('app') 8 | .config(function (contentfulProvider) { 9 | contentfulProvider.setOptions({ 10 | space: 'cfexampleapi', 11 | accessToken: 'b4c0n73n7fu1' 12 | }); 13 | }); 14 | 15 | })(); 16 | -------------------------------------------------------------------------------- /examples/how-to-use-the-directives/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Simple demo application 4 | 5 | 6 | 7 | 8 | 9 |

Directives demo

10 |

This simple demo uses the following Contentful configuration:

11 |
    12 |
  • Space: cfexampleapi
  • 13 |
  • Access token: b4c0n73n7fu1
  • 14 |
15 |

Get all dogs

16 |
    17 |
  • 18 | {{ dog.fields.name }} 19 |
  • 20 |
21 |

Get one entry

22 |
{{ $contentfulEntry | json }}
23 |

Get one entry and resolve links

24 |
{{ $contentfulEntry | json }}
25 |

Get all entries

26 |
27 |

{{ $contentfulEntries.total }} items found:

28 |
    29 |
  • 30 |
    {{ entry.sys.id }}
    31 |
  • 32 |
33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var karma = require('karma').server; 3 | var concat = require('gulp-concat'); 4 | var uglify = require('gulp-uglify'); 5 | var rename = require('gulp-rename'); 6 | var path = require('path'); 7 | var plumber = require('gulp-plumber'); 8 | var runSequence = require('run-sequence'); 9 | var jshint = require('gulp-jshint'); 10 | 11 | /** 12 | * File patterns 13 | **/ 14 | 15 | // Root directory 16 | var rootDirectory = path.resolve('./'); 17 | 18 | // Source directory for build process 19 | var sourceDirectory = path.join(rootDirectory, './src'); 20 | 21 | var sourceFiles = [ 22 | 23 | // Make sure module files are handled first 24 | path.join(sourceDirectory, '/**/*.module.js'), 25 | 26 | // Then add all JavaScript files 27 | path.join(sourceDirectory, '/**/*.js') 28 | ]; 29 | 30 | gulp.task('build', function() { 31 | gulp.src(sourceFiles) 32 | .pipe(plumber()) 33 | .pipe(concat('angular-contentful.js')) 34 | .pipe(gulp.dest('./dist/')) 35 | .pipe(uglify()) 36 | .pipe(rename('angular-contentful.min.js')) 37 | .pipe(gulp.dest('./dist')) 38 | }); 39 | 40 | /** 41 | * Process 42 | */ 43 | gulp.task('process-all', function (done) { 44 | runSequence('jshint-src', 'test-src', 'build', done) 45 | }); 46 | 47 | /** 48 | * Watch task 49 | */ 50 | gulp.task('watch', function () { 51 | 52 | // Watch JavaScript files 53 | gulp.watch(sourceFiles, ['process-all']); 54 | }); 55 | 56 | /** 57 | * Validate source JavaScript 58 | */ 59 | 60 | gulp.task('jshint-src', function () { 61 | return gulp.src(sourceFiles) 62 | .pipe(plumber()) 63 | .pipe(jshint()) 64 | .pipe(jshint.reporter('jshint-stylish')) 65 | .pipe(jshint.reporter('fail')) 66 | }); 67 | 68 | /** 69 | * Run test once and exit 70 | */ 71 | gulp.task('test-src', function (done) { 72 | karma.start({ 73 | configFile: __dirname + '/karma-src.conf.js', 74 | singleRun: true 75 | }, done); 76 | }); 77 | 78 | /** 79 | * Run test once and exit 80 | */ 81 | gulp.task('test-dist-concatenated', function (done) { 82 | karma.start({ 83 | configFile: __dirname + '/karma-dist-concatenated.conf.js', 84 | singleRun: true 85 | }, done); 86 | }); 87 | 88 | /** 89 | * Run test once and exit 90 | */ 91 | gulp.task('test-dist-minified', function (done) { 92 | karma.start({ 93 | configFile: __dirname + '/karma-dist-minified.conf.js', 94 | singleRun: true 95 | }, done); 96 | }); 97 | 98 | gulp.task('default', function () { 99 | runSequence('process-all', 'watch') 100 | }); 101 | -------------------------------------------------------------------------------- /karma-dist-concatenated.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Thu Aug 21 2014 10:24:39 GMT+0200 (CEST) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['mocha', 'chai-jquery', 'jquery-1.8.3', 'sinon-chai'], 14 | 15 | plugins: [ 16 | 'karma-mocha', 17 | 'karma-chai', 18 | 'karma-sinon-chai', 19 | 'karma-chrome-launcher', 20 | 'karma-phantomjs-launcher', 21 | 'karma-jquery', 22 | 'karma-chai-jquery' 23 | ], 24 | 25 | // list of files / patterns to load in the browser 26 | files: [ 27 | 'bower/angular/angular.js', 28 | 'bower/angular-mocks/angular-mocks.js', 29 | 'dist/angular-contentful.js', 30 | 'test/unit/**/*.js' 31 | ], 32 | 33 | 34 | // list of files to exclude 35 | exclude: [ 36 | ], 37 | 38 | 39 | // preprocess matching files before serving them to the browser 40 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 41 | preprocessors: { 42 | }, 43 | 44 | 45 | // test results reporter to use 46 | // possible values: 'dots', 'progress' 47 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 48 | reporters: ['progress'], 49 | 50 | 51 | // web server port 52 | port: 9876, 53 | 54 | 55 | // enable / disable colors in the output (reporters and logs) 56 | colors: true, 57 | 58 | 59 | // level of logging 60 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 61 | logLevel: config.LOG_INFO, 62 | 63 | 64 | // enable / disable watching file and executing tests whenever any file changes 65 | autoWatch: true, 66 | 67 | 68 | // start these browsers 69 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 70 | browsers: ['PhantomJS'], 71 | 72 | 73 | // Continuous Integration mode 74 | // if true, Karma captures browsers, runs the tests and exits 75 | singleRun: false 76 | }); 77 | }; 78 | -------------------------------------------------------------------------------- /karma-dist-minified.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Thu Aug 21 2014 10:24:39 GMT+0200 (CEST) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['mocha', 'chai-jquery', 'jquery-1.8.3', 'sinon-chai'], 14 | 15 | plugins: [ 16 | 'karma-mocha', 17 | 'karma-chai', 18 | 'karma-sinon-chai', 19 | 'karma-chrome-launcher', 20 | 'karma-phantomjs-launcher', 21 | 'karma-jquery', 22 | 'karma-chai-jquery' 23 | ], 24 | 25 | // list of files / patterns to load in the browser 26 | files: [ 27 | 'bower/angular/angular.js', 28 | 'bower/angular-mocks/angular-mocks.js', 29 | 'dist/angular-contentful.min.js', 30 | 'test/unit/**/*.js' 31 | ], 32 | 33 | 34 | // list of files to exclude 35 | exclude: [ 36 | ], 37 | 38 | 39 | // preprocess matching files before serving them to the browser 40 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 41 | preprocessors: { 42 | }, 43 | 44 | 45 | // test results reporter to use 46 | // possible values: 'dots', 'progress' 47 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 48 | reporters: ['progress'], 49 | 50 | 51 | // web server port 52 | port: 9876, 53 | 54 | 55 | // enable / disable colors in the output (reporters and logs) 56 | colors: true, 57 | 58 | 59 | // level of logging 60 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 61 | logLevel: config.LOG_INFO, 62 | 63 | 64 | // enable / disable watching file and executing tests whenever any file changes 65 | autoWatch: true, 66 | 67 | 68 | // start these browsers 69 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 70 | browsers: ['PhantomJS'], 71 | 72 | 73 | // Continuous Integration mode 74 | // if true, Karma captures browsers, runs the tests and exits 75 | singleRun: false 76 | }); 77 | }; 78 | -------------------------------------------------------------------------------- /karma-src.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Thu Aug 21 2014 10:24:39 GMT+0200 (CEST) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['mocha', 'chai-jquery', 'jquery-1.8.3', 'sinon-chai'], 14 | 15 | plugins: [ 16 | 'karma-mocha', 17 | 'karma-chai', 18 | 'karma-sinon-chai', 19 | 'karma-chrome-launcher', 20 | 'karma-phantomjs-launcher', 21 | 'karma-jquery', 22 | 'karma-chai-jquery' 23 | ], 24 | 25 | // list of files / patterns to load in the browser 26 | files: [ 27 | 'bower/angular/angular.js', 28 | 'bower/angular-mocks/angular-mocks.js', 29 | 'src/**/*.module.js', 30 | 'src/**/*.js', 31 | 'test/unit/**/*.js' 32 | ], 33 | 34 | 35 | // list of files to exclude 36 | exclude: [ 37 | ], 38 | 39 | 40 | // preprocess matching files before serving them to the browser 41 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 42 | preprocessors: { 43 | }, 44 | 45 | 46 | // test results reporter to use 47 | // possible values: 'dots', 'progress' 48 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 49 | reporters: ['progress'], 50 | 51 | 52 | // web server port 53 | port: 9876, 54 | 55 | 56 | // enable / disable colors in the output (reporters and logs) 57 | colors: true, 58 | 59 | 60 | // level of logging 61 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 62 | logLevel: config.LOG_INFO, 63 | 64 | 65 | // enable / disable watching file and executing tests whenever any file changes 66 | autoWatch: true, 67 | 68 | 69 | // start these browsers 70 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 71 | browsers: ['PhantomJS'], 72 | 73 | 74 | // Continuous Integration mode 75 | // if true, Karma captures browsers, runs the tests and exits 76 | singleRun: false 77 | }); 78 | }; 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-contentful", 3 | "version": "2.2.0", 4 | "author": { 5 | "name": "Jurgen Van de Moere", 6 | "email": "jurgen.van.de.moere@gmail.com" 7 | }, 8 | "main": "dist/angular-contentful.js", 9 | "module": "dist/angular-contentful.js", 10 | "scripts": { 11 | "test": "gulp test-dist-minified" 12 | }, 13 | "dependencies": {}, 14 | "devDependencies": { 15 | "chai": "^1.9.1", 16 | "chai-jquery": "^1.2.3", 17 | "gulp": "^3.8.7", 18 | "gulp-concat": "^2.3.4", 19 | "gulp-jshint": "^1.8.4", 20 | "gulp-plumber": "^0.6.6", 21 | "gulp-rename": "^1.2.0", 22 | "gulp-uglify": "^0.3.1", 23 | "jshint-stylish": "^0.4.0", 24 | "karma": "^0.12.22", 25 | "karma-chai": "^0.1.0", 26 | "karma-chai-jquery": "^1.0.0", 27 | "karma-chrome-launcher": "^0.1.4", 28 | "karma-jasmine": "^0.1.5", 29 | "karma-jquery": "^0.1.0", 30 | "karma-mocha": "^0.1.8", 31 | "karma-phantomjs-launcher": "^0.1.4", 32 | "karma-sinon-chai": "^0.2.0", 33 | "mocha": "^1.21.4", 34 | "run-sequence": "^1.0.2", 35 | "sinon": "^1.10.3", 36 | "sinon-chai": "^2.5.0" 37 | }, 38 | "engines": { 39 | "node": ">=0.8.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/contentful/contentful.module.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | // Modules 4 | angular.module('contentful', []); 5 | 6 | })(); 7 | -------------------------------------------------------------------------------- /src/contentful/controllers/contentful-directive.controller.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | /** 4 | * Controller for contentful directives 5 | * 6 | * By separating the controller we can avoid a lot of 7 | * repetitive code and keep the code base as small as 8 | * possible. 9 | * 10 | * @param $attrs 11 | * @param contentful 12 | * @constructor 13 | */ 14 | function ContentfulDirectiveCtrl($scope, $attrs, contentful, contentfulHelpers) { 15 | 16 | var query; 17 | 18 | // Passed value is required entry id 19 | if ($attrs.contentfulEntry) { 20 | 21 | query = $scope.$eval($attrs.contentfulEntry); 22 | 23 | // In case we detect a query string instead of simple id, we fetch the 24 | // collection and return the first entry 25 | if(contentfulHelpers.isQueryString(query)){ 26 | 27 | // Fetch entry by query 28 | contentful 29 | .entries(query) 30 | .then( 31 | function (response) { 32 | var firstEntry = {}; 33 | if(response.data && response.data.items && response.data.items.length){ 34 | firstEntry = response.data.items[0]; 35 | } 36 | $scope.$contentfulEntry = firstEntry; 37 | }, 38 | function(){ 39 | $scope.$contentfulEntry = {}; 40 | } 41 | ); 42 | 43 | } else { 44 | 45 | // Fetch entry by id 46 | contentful 47 | .entry(query) 48 | .then( 49 | function (response) { 50 | $scope.$contentfulEntry = response.data; 51 | }, 52 | function(){ 53 | $scope.$contentfulEntry = {}; 54 | } 55 | ); 56 | 57 | } 58 | 59 | } 60 | 61 | // Passed value is optional query 62 | if ($attrs.hasOwnProperty('contentfulEntries')) { 63 | 64 | query = $scope.$eval($attrs.contentfulEntries); 65 | 66 | contentful 67 | .entries(query) 68 | .then( 69 | function (response) { 70 | $scope.$contentfulEntries = response.data; 71 | }, 72 | function(){ 73 | $scope.$contentfulEntries = { 74 | limit: 0, 75 | skip: 0, 76 | total: 0, 77 | items: [] 78 | }; 79 | } 80 | ); 81 | } 82 | 83 | } 84 | 85 | // Inject controller dependencies 86 | ContentfulDirectiveCtrl.$inject = ['$scope', '$attrs', 'contentful', 'contentfulHelpers']; 87 | 88 | // Export 89 | angular 90 | .module('contentful') 91 | .controller('ContentfulDirectiveCtrl', ContentfulDirectiveCtrl); 92 | 93 | })(); 94 | -------------------------------------------------------------------------------- /src/contentful/directives/contentful-entries.directive.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | /** 4 | * Directive 5 | * 6 | * @returns {object} directive definition object 7 | */ 8 | function contentfulEntriesDirective() { 9 | 10 | return { 11 | restrict: 'EA', 12 | scope: true, 13 | controller: 'ContentfulDirectiveCtrl' 14 | }; 15 | 16 | } 17 | 18 | // Inject directive dependencies 19 | contentfulEntriesDirective.$inject = []; 20 | 21 | // Export 22 | angular 23 | .module('contentful') 24 | .directive('contentfulEntries', contentfulEntriesDirective); 25 | 26 | })(); 27 | -------------------------------------------------------------------------------- /src/contentful/directives/contentful-entry.directive.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | /** 4 | * Directive 5 | * 6 | * @returns {object} directive definition object 7 | */ 8 | function contentfulEntryDirective() { 9 | 10 | return { 11 | restrict: 'EA', 12 | scope: true, 13 | controller: 'ContentfulDirectiveCtrl' 14 | }; 15 | 16 | } 17 | 18 | // Inject directive dependencies 19 | contentfulEntryDirective.$inject = []; 20 | 21 | // Export 22 | angular 23 | .module('contentful') 24 | .directive('contentfulEntry', contentfulEntryDirective); 25 | 26 | })(); 27 | -------------------------------------------------------------------------------- /src/contentful/services/contentful-helpers.service.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | function contentfulHelpersFactory() { 4 | 5 | function ContentfulHelpers() { 6 | } 7 | 8 | /** 9 | * Resolve a complete response 10 | * 11 | * @param response 12 | * @returns {Array} 13 | */ 14 | ContentfulHelpers.prototype.resolveResponse = function resolveResponse(response) { 15 | var self = this; 16 | self.walkMutate(response, self.isLink, function (link) { 17 | return self.getLink(response, link) || link; 18 | }); 19 | return response.items || []; 20 | }; 21 | 22 | /** 23 | * Check if object is a link 24 | * 25 | * @param {object} 26 | * @returns {boolean} 27 | */ 28 | ContentfulHelpers.prototype.isLink = function isLink(object) { 29 | if (object && object.sys && object.sys.type && object.sys.type === 'Link') { 30 | return true; 31 | } 32 | return false; 33 | }; 34 | 35 | /** 36 | * Find and return a link in a response 37 | * 38 | * @param response 39 | * @param link 40 | * @returns {object|null} Link 41 | */ 42 | ContentfulHelpers.prototype.getLink = function getLink(response, link) { 43 | var self = this; 44 | var type = link.sys.linkType; 45 | var id = link.sys.id; 46 | var pred = function (resource) { 47 | return resource && resource.sys && resource.sys.type === type && resource.sys.id === id; 48 | }; 49 | return self.findLink(response.items, pred) || 50 | response.includes && self.findLink(response.includes[type], pred); 51 | }; 52 | 53 | /** 54 | * Helper method to find a link in an array 55 | * 56 | * @param {Array} arr - Array to search 57 | * @param {function} pred - Predicate function 58 | * @returns {object|null} Link 59 | */ 60 | ContentfulHelpers.prototype.findLink = function findLink(arr, pred) { 61 | var i; 62 | var link = null; 63 | if (!angular.isArray(arr)) { 64 | return link; 65 | } 66 | for (i = 0; i < arr.length; i++) { 67 | if (pred(arr[i])) { 68 | link = arr[i]; 69 | break; 70 | } 71 | } 72 | return link; 73 | }; 74 | 75 | /** 76 | * Walk a data structure and mutate properties that match the predicate function 77 | * 78 | * @param {object|array} input - Input data 79 | * @param {function} pred - Prediction function 80 | * @param {function} mutator - Mutator function 81 | * @returns {*} 82 | */ 83 | ContentfulHelpers.prototype.walkMutate = function walkMutate(input, pred, mutator) { 84 | var self = this; 85 | if (pred(input)){ 86 | return mutator(input); 87 | } 88 | 89 | if (angular.isArray(input) || angular.isObject(input)) { 90 | angular.forEach(input, function (item, key) { 91 | input[key] = self.walkMutate(item, pred, mutator); 92 | }); 93 | return input; 94 | } 95 | return input; 96 | }; 97 | 98 | 99 | /** 100 | * Check if a string is a query string 101 | * 102 | * @param {string} input 103 | * @returns {boolean} 104 | */ 105 | ContentfulHelpers.prototype.isQueryString = function isQueryString(input) { 106 | if(input.toString().indexOf('=') > -1){ 107 | return true; 108 | } 109 | if(input.toString().indexOf('&') > -1){ 110 | return true; 111 | } 112 | if(input.toString().indexOf('?') > -1){ 113 | return true; 114 | } 115 | return false; 116 | }; 117 | 118 | 119 | 120 | return new ContentfulHelpers(); 121 | 122 | } 123 | 124 | // Export 125 | angular 126 | .module('contentful') 127 | .factory('contentfulHelpers', contentfulHelpersFactory); 128 | 129 | })(); 130 | -------------------------------------------------------------------------------- /src/contentful/services/contentful.service.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | /** 4 | * Contentful service provider 5 | */ 6 | function contentfulProvider() { 7 | 8 | // Default options 9 | var options = { 10 | 'default': { 11 | host: 'cdn.contentful.com', 12 | space: null, 13 | accessToken: null, 14 | secure: true 15 | } 16 | }; 17 | 18 | /** 19 | * Set options 20 | * 21 | * @param {object} newOptions 22 | * @returns {contentfulProvider} 23 | */ 24 | this.setOptions = function (newOptions) { 25 | if (newOptions && newOptions.space) { 26 | angular.extend(options['default'], newOptions); 27 | } else { 28 | angular.extend(options, newOptions); 29 | } 30 | return this; 31 | }; 32 | 33 | this.$get = contentfulFactory; 34 | 35 | /** 36 | * Create the contentful service 37 | * 38 | * @returns {contentfulProvider.Contentful} 39 | */ 40 | function contentfulFactory($http, $q, contentfulHelpers) { 41 | return new Contentful($http, $q, contentfulHelpers, options); 42 | } 43 | 44 | // Inject dependencies in factory 45 | contentfulFactory.$inject = ['$http', '$q', 'contentfulHelpers']; 46 | 47 | /** 48 | * Contentful service constructor 49 | * 50 | * @constructor 51 | */ 52 | function Contentful($http, $q, contentfulHelpers, options) { 53 | 54 | this._$http = $http; 55 | this._$q = $q; 56 | this._contentfulHelpers = contentfulHelpers; 57 | this.options = options; 58 | 59 | if (typeof $http.get !== 'function') { 60 | throw new Error('The contentful service needs a valid http service to work with'); 61 | } 62 | 63 | if (typeof $q.when !== 'function') { 64 | throw new Error('The contentful service needs a valid promise service to work with'); 65 | } 66 | } 67 | 68 | /** 69 | * Perform request 70 | * 71 | * @param {string} path 72 | * @param {object} config 73 | * @param {object} optionSet 74 | * @returns {promise} 75 | */ 76 | Contentful.prototype.request = function (path, config, optionSet) { 77 | optionSet = optionSet || 'default'; 78 | var url; 79 | var nonEmptyParams = {}; 80 | 81 | // Make sure config is valid 82 | config = config || {}; 83 | config.headers = config.headers || {}; 84 | config.params = config.params || {}; 85 | 86 | // Add required configuration 87 | config.headers['Content-Type'] = 'application/vnd.contentful.delivery.v1+json'; 88 | config.params['access_token'] = this.options[optionSet].accessToken; 89 | 90 | // Build url 91 | url = [ 92 | this.options[optionSet].secure ? 'https' : 'http', 93 | '://', 94 | this.options[optionSet].host, 95 | ':', 96 | this.options[optionSet].secure ? '443' : '80', 97 | '/spaces/', 98 | this.options[optionSet].space, 99 | path 100 | ].join(''); 101 | 102 | // Perform request and return promise 103 | return this._$http.get(url, config); 104 | }; 105 | 106 | /** 107 | * Get an asset 108 | * 109 | * @param id 110 | * @param optionSet {object} 111 | * @returns {promise} 112 | */ 113 | Contentful.prototype.asset = function (id, optionSet) { 114 | optionSet = optionSet || 'default'; 115 | return this.request('/assets/' + id, {}, optionSet); 116 | }; 117 | 118 | /** 119 | * Get assets 120 | * 121 | * @param query 122 | * @param optionSet {object} 123 | * @returns {promise} 124 | */ 125 | Contentful.prototype.assets = function (querystring, optionSet) { 126 | optionSet = optionSet || 'default'; 127 | return this.processResponseWithMultipleEntries( 128 | this.request('/assets', configifyParams(paramifyQuerystring(querystring)), optionSet) 129 | ); 130 | }; 131 | 132 | /** 133 | * Get content type 134 | * 135 | * @param id 136 | * @param optionSet {object} 137 | * @returns {promise} 138 | */ 139 | Contentful.prototype.contentType = function (id, optionSet) { 140 | optionSet = optionSet || 'default'; 141 | return this.request('/content_types/' + id, {}, optionSet); 142 | }; 143 | 144 | /** 145 | * Get content types 146 | * 147 | * @param query 148 | * @param optionSet {object} 149 | * @returns {promise} 150 | */ 151 | Contentful.prototype.contentTypes = function (querystring, optionSet) { 152 | optionSet = optionSet || 'default'; 153 | return this.processResponseWithMultipleEntries( 154 | this.request('/content_types', configifyParams(paramifyQuerystring(querystring)), optionSet) 155 | ); 156 | }; 157 | 158 | /** 159 | * Get entry 160 | * 161 | * @param id 162 | * @param optionSet {object} 163 | * @returns {promise} 164 | */ 165 | Contentful.prototype.entry = function (id, optionSet) { 166 | optionSet = optionSet || 'default'; 167 | return this.request('/entries/' + id, {}, optionSet); 168 | }; 169 | 170 | /** 171 | * Get entries 172 | * 173 | * @param query 174 | * @param optionSet {object} 175 | * @returns {promise} 176 | */ 177 | Contentful.prototype.entries = function (querystring, optionSet) { 178 | optionSet = optionSet || 'default'; 179 | return this.processResponseWithMultipleEntries( 180 | this.request('/entries', configifyParams(paramifyQuerystring(querystring)), optionSet) 181 | ); 182 | }; 183 | 184 | /** 185 | * Get space 186 | * 187 | * @param optionSet {object} 188 | * @returns {promise} 189 | */ 190 | Contentful.prototype.space = function (optionSet) { 191 | optionSet = optionSet || 'default'; 192 | return this.request('', {}, optionSet); 193 | }; 194 | 195 | /** 196 | * Process multiple incoming entries 197 | * 198 | * Resolves links in the response.data. 199 | * 200 | * @param promise 201 | * @returns {*} 202 | */ 203 | Contentful.prototype.processResponseWithMultipleEntries = function(promise){ 204 | var self = this; 205 | if(promise && promise.then){ 206 | promise.then( 207 | 208 | // Automatically resolve links on success 209 | function(response){ 210 | var entries = { 211 | limit: response.data.limit, 212 | skip: response.data.skip, 213 | total: response.data.total 214 | }; 215 | entries.items = self._contentfulHelpers.resolveResponse(response.data); 216 | response.data = entries; 217 | return response; 218 | }, 219 | 220 | // Forward error on failure 221 | function(response){ 222 | return response; 223 | } 224 | ); 225 | } 226 | return promise; 227 | }; 228 | 229 | /** 230 | * Process single incoming entry 231 | * 232 | * For now, this is just a noop but it exists so it makes 233 | * sure a $q promise is returned with only then method 234 | * (removing shorthand success and error methods) 235 | * and to have single point of entry in case transformation 236 | * is needed in the future. 237 | * 238 | * @param promise 239 | * @returns {*} 240 | */ 241 | Contentful.prototype.processResponseWithSingleEntry = function(promise){ 242 | if(promise && promise.then){ 243 | promise.then( 244 | 245 | // Forward error on failure 246 | function(response){ 247 | return response; 248 | }, 249 | 250 | // Forward error on failure 251 | function(response){ 252 | return response; 253 | } 254 | ); 255 | } 256 | return promise; 257 | }; 258 | 259 | } 260 | 261 | /** 262 | * Create params object from querystring 263 | * 264 | * @param querystring 265 | * @returns {object} params 266 | */ 267 | function paramifyQuerystring(querystring){ 268 | var params = {}; 269 | 270 | if(!querystring){ 271 | return params; 272 | } 273 | 274 | // Split querystring in parts separated by '&' 275 | var couples = querystring.toString().split('&'); 276 | angular.forEach(couples, function(couple){ 277 | 278 | // Split in parts separated by '=' 279 | var parts = couple.split('='); 280 | 281 | // Only add if an actual value is passed 282 | // to prevent empty params in the url 283 | if(parts.length > 1){ 284 | params[parts[0]] = parts[1]; 285 | } 286 | }); 287 | return params; 288 | } 289 | 290 | /** 291 | * Create config object from params 292 | * 293 | * @param params 294 | * @returns {object} config 295 | */ 296 | function configifyParams(params){ 297 | if(!angular.isObject(params)){ 298 | params = {}; 299 | } 300 | return { 301 | params: params 302 | }; 303 | } 304 | 305 | // Export 306 | angular 307 | .module('contentful') 308 | .provider('contentful', contentfulProvider); 309 | 310 | })(); 311 | -------------------------------------------------------------------------------- /test/unit/contentful/contentful.module.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Contentful module', function() { 4 | 5 | var module; 6 | 7 | beforeEach(function() { 8 | // Get module 9 | module = angular.module('contentful'); 10 | }); 11 | 12 | it('should exist', function() { 13 | expect(module).to.be.ok; 14 | }); 15 | 16 | }); 17 | -------------------------------------------------------------------------------- /test/unit/contentful/directives/contentful-entries.directive.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('contentfulEntries directive', function () { 4 | 5 | var contentfulProvider; 6 | var $httpBackend; 7 | var $compile; 8 | var $rootScope; 9 | var customOptions = { 10 | space: 'dummySpace', 11 | accessToken: 'dummyAccessToken', 12 | secure: true 13 | }; 14 | 15 | var sampleResponse = { 16 | items: [ 17 | { 18 | someValue: 'wow', 19 | someLink: {sys: {type: 'Link', linkType: 'Entry', id: 'suchId'}} 20 | } 21 | ], 22 | includes: { 23 | Entry: [ 24 | {sys: {type: 'Entry', id: 'suchId'}, very: 'doge'} 25 | ] 26 | } 27 | }; 28 | 29 | var expectedOutcome = [ 30 | { 31 | // Value stays the same 32 | someValue: 'wow', 33 | 34 | // Link gets replaced by the actual object from `includes.Entry` 35 | someLink: {sys: {type: 'Entry', id: 'suchId'}, very: 'doge'} 36 | } 37 | ]; 38 | 39 | // Expected host to check against when using custom options above 40 | var expectedHost = 'https://cdn.contentful.com:443'; 41 | 42 | beforeEach(function () { 43 | 44 | // Define a fake module so we can configure the provider 45 | // before injecting the service 46 | var fakeModule = angular.module('fakeModule', []); 47 | fakeModule.config(function (_contentfulProvider_) { 48 | contentfulProvider = _contentfulProvider_; 49 | contentfulProvider.setOptions(customOptions); 50 | }); 51 | 52 | // Load the module 53 | module('contentful', 'fakeModule'); 54 | 55 | // Instantiate the service 56 | inject(function (_$httpBackend_, _$compile_, _$rootScope_) { 57 | $httpBackend = _$httpBackend_; 58 | $compile = _$compile_; 59 | $rootScope = _$rootScope_; 60 | }); 61 | 62 | }); 63 | 64 | afterEach(function () { 65 | $httpBackend.verifyNoOutstandingExpectation(); 66 | $httpBackend.verifyNoOutstandingRequest(); 67 | }); 68 | 69 | it('should perform a request to the correct API endpoint when no query string is passed', function () { 70 | 71 | var markup = '
'; 72 | 73 | $httpBackend 74 | .expectGET(expectedHost + '/spaces/dummySpace/entries?access_token=dummyAccessToken') 75 | .respond(200, ''); 76 | 77 | var element = $compile(markup)($rootScope); 78 | $rootScope.$digest(); 79 | $httpBackend.flush(); 80 | }); 81 | 82 | it('should perform a request to the correct API endpoint when invalid query string is passed', function () { 83 | 84 | var queryString = 'customQuery'; 85 | var markup = '
{{$contentfulEntries}}
'; 86 | 87 | $httpBackend 88 | .expectGET(expectedHost + '/spaces/dummySpace/entries?access_token=dummyAccessToken') 89 | .respond(200, 'fake-response'); 90 | 91 | var element = $compile(markup)($rootScope); 92 | $rootScope.$digest(); 93 | $httpBackend.flush(); 94 | }); 95 | 96 | it('should perform a request to the correct API endpoint when valid query string is passed', function () { 97 | 98 | var queryString = 'query=test'; 99 | var markup = '
{{$contentfulEntries}}
'; 100 | 101 | $httpBackend 102 | .expectGET(expectedHost + '/spaces/dummySpace/entries?access_token=dummyAccessToken&query=test') 103 | .respond(200, 'fake-response'); 104 | 105 | var element = $compile(markup)($rootScope); 106 | $rootScope.$digest(); 107 | $httpBackend.flush(); 108 | }); 109 | 110 | it('should perform a request to the correct API endpoint when an expression is passed', function () { 111 | 112 | var queryString = 'query=test'; 113 | var $scope = $rootScope.$new(); 114 | $scope.query = queryString; 115 | var markup = '
{{$contentfulEntries}}
'; 116 | 117 | $httpBackend 118 | .expectGET(expectedHost + '/spaces/dummySpace/entries?access_token=dummyAccessToken&query=test') 119 | .respond(200, 'fake-response'); 120 | 121 | var element = $compile(markup)($scope); 122 | $rootScope.$digest(); 123 | $httpBackend.flush(); 124 | }); 125 | 126 | it('should make the entries available as $contentfulEntries', function () { 127 | 128 | var markup = '
{{$contentfulEntries}}
'; 129 | 130 | $httpBackend 131 | .expectGET(expectedHost + '/spaces/dummySpace/entries?access_token=dummyAccessToken') 132 | .respond(200, sampleResponse); 133 | 134 | var element = $compile(markup)($rootScope); 135 | $rootScope.$digest(); 136 | $httpBackend.flush(); 137 | 138 | expect(element.html()).to.contain(JSON.stringify(expectedOutcome)); 139 | }); 140 | 141 | }); 142 | -------------------------------------------------------------------------------- /test/unit/contentful/directives/contentful-entry.directive.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('contentfulEntry directive', function () { 4 | 5 | var contentfulProvider; 6 | var $httpBackend; 7 | var $compile; 8 | var $rootScope; 9 | var customOptions = { 10 | space: 'dummySpace', 11 | accessToken: 'dummyAccessToken', 12 | secure: true 13 | }; 14 | 15 | // Expected host to check against when using custom options above 16 | var expectedHost = 'https://cdn.contentful.com:443'; 17 | 18 | beforeEach(function () { 19 | 20 | // Define a fake module so we can configure the provider 21 | // before injecting the service 22 | var fakeModule = angular.module('fakeModule', []); 23 | fakeModule.config(function (_contentfulProvider_) { 24 | contentfulProvider = _contentfulProvider_; 25 | contentfulProvider.setOptions(customOptions); 26 | }); 27 | 28 | // Load the module 29 | module('contentful', 'fakeModule'); 30 | 31 | // Instantiate the service 32 | inject(function (_$httpBackend_, _$compile_, _$rootScope_) { 33 | $httpBackend = _$httpBackend_; 34 | $compile = _$compile_; 35 | $rootScope = _$rootScope_; 36 | }); 37 | 38 | }); 39 | 40 | afterEach(function () { 41 | $httpBackend.verifyNoOutstandingExpectation(); 42 | $httpBackend.verifyNoOutstandingRequest(); 43 | }); 44 | 45 | it('should perform a request to the correct API endpoint when id is specified', function () { 46 | 47 | var id = 'customId'; 48 | var markup = '
'; 49 | 50 | $httpBackend 51 | .expectGET(expectedHost + '/spaces/dummySpace/entries/' + id + '?access_token=dummyAccessToken') 52 | .respond(200, 'fake-response'); 53 | 54 | var element = $compile(markup)($rootScope); 55 | $rootScope.$digest(); 56 | $httpBackend.flush(); 57 | }); 58 | 59 | it('should perform a request to the correct API endpoint when an expression is specified', function () { 60 | 61 | var id = 'customId'; 62 | var $scope = $rootScope.$new(); 63 | $scope.entryId = id; 64 | var markup = '
'; 65 | 66 | $httpBackend 67 | .expectGET(expectedHost + '/spaces/dummySpace/entries/' + id + '?access_token=dummyAccessToken') 68 | .respond(200, 'fake-response'); 69 | 70 | var element = $compile(markup)($scope); 71 | $rootScope.$digest(); 72 | $httpBackend.flush(); 73 | }); 74 | 75 | it('should perform a request to the correct API endpoint when query string is specified', function () { 76 | 77 | var queryString = 'content_type=dog&fields.name=bob'; 78 | var markup = '
'; 79 | 80 | $httpBackend 81 | .expectGET(expectedHost + '/spaces/dummySpace/entries?access_token=dummyAccessToken&' + queryString) 82 | .respond(200, 'fake-response'); 83 | 84 | var element = $compile(markup)($rootScope); 85 | $rootScope.$digest(); 86 | $httpBackend.flush(); 87 | }); 88 | 89 | it('should make the entry available as $contentfulEntry', function () { 90 | 91 | var id = 'customId'; 92 | var markup = '
{{$contentfulEntry}}
'; 93 | 94 | $httpBackend 95 | .expectGET(expectedHost + '/spaces/dummySpace/entries/' + id + '?access_token=dummyAccessToken') 96 | .respond(200, 'fake-response'); 97 | 98 | var element = $compile(markup)($rootScope); 99 | $rootScope.$digest(); 100 | $httpBackend.flush(); 101 | 102 | expect(element.html()).to.contain('fake-response'); 103 | }); 104 | 105 | }); 106 | -------------------------------------------------------------------------------- /test/unit/contentful/services/contentful-helpers.service.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('contentfulHelpers service', function () { 4 | 5 | var contentfulHelpers; 6 | 7 | var sampleResponse = { 8 | items: [ 9 | { 10 | someValue: 'wow', 11 | someLink: {sys: {type: 'Link', linkType: 'Entry', id: 'suchId'}} 12 | } 13 | ], 14 | includes: { 15 | Entry: [ 16 | {sys: {type: 'Entry', id: 'suchId'}, very: 'doge'} 17 | ] 18 | } 19 | }; 20 | 21 | var expectedOutcome = [ 22 | { 23 | // Value stays the same 24 | someValue: 'wow', 25 | 26 | // Link gets replaced by the actual object from `includes.Entry` 27 | someLink: {sys: {type: 'Entry', id: 'suchId'}, very: 'doge'} 28 | } 29 | ]; 30 | 31 | beforeEach(module('contentful')); 32 | 33 | beforeEach(inject(function(_contentfulHelpers_){ 34 | contentfulHelpers = _contentfulHelpers_; 35 | })); 36 | 37 | describe('resolveResponse', function () { 38 | 39 | it('should be a method', function () { 40 | expect(contentfulHelpers.resolveResponse).to.be.a('function'); 41 | }); 42 | 43 | it('should return the expected object', function () { 44 | expect(contentfulHelpers.resolveResponse(sampleResponse)).to.deep.equal(expectedOutcome); 45 | }); 46 | 47 | }); 48 | 49 | describe('isQueryString', function () { 50 | 51 | it('should be a method', function () { 52 | expect(contentfulHelpers.isQueryString).to.be.a('function'); 53 | }); 54 | 55 | }); 56 | 57 | describe('isQueryString(id)', function () { 58 | 59 | it('should return false', function () { 60 | expect(contentfulHelpers.isQueryString('31Eu8SwBxCEuSOIWEu8wUM')).to.equal(false); 61 | }); 62 | 63 | }); 64 | 65 | describe('isQueryString(stringwith?)', function () { 66 | 67 | it('should return false', function () { 68 | expect(contentfulHelpers.isQueryString('stringwith?')).to.equal(true); 69 | }); 70 | 71 | }); 72 | 73 | describe('isQueryString(stringwith=)', function () { 74 | 75 | it('should return false', function () { 76 | expect(contentfulHelpers.isQueryString('stringwith=')).to.equal(true); 77 | }); 78 | 79 | }); 80 | 81 | describe('isQueryString(stringwith&)', function () { 82 | 83 | it('should return false', function () { 84 | expect(contentfulHelpers.isQueryString('stringwith&')).to.equal(true); 85 | }); 86 | 87 | }); 88 | 89 | 90 | }); 91 | -------------------------------------------------------------------------------- /test/unit/contentful/services/contentful.service.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Contentful service', function () { 4 | 5 | var contentful; 6 | var contentfulProvider; 7 | var $http; 8 | var $httpBackend; 9 | var $q; 10 | var customOptions = { 11 | space: 'dummySpace', 12 | accessToken: 'dummyAccessToken', 13 | secure: true 14 | }; 15 | 16 | // Expected host to check against when using custom options above 17 | var expectedHost = 'https://cdn.contentful.com:443'; 18 | 19 | beforeEach(function () { 20 | 21 | // Define a fake module so we can configure the provider 22 | // before injecting the service 23 | var fakeModule = angular.module('fakeModule', []); 24 | fakeModule.config(function (_contentfulProvider_) { 25 | contentfulProvider = _contentfulProvider_; 26 | contentfulProvider.setOptions(customOptions); 27 | }); 28 | 29 | // Load the module 30 | module('contentful', 'fakeModule'); 31 | 32 | // Instantiate the service 33 | inject(function (_$http_, _$q_, _contentful_, _$httpBackend_) { 34 | $http = _$http_; 35 | $q = _$q_; 36 | contentful = _contentful_; 37 | $httpBackend = _$httpBackend_; 38 | }); 39 | 40 | }); 41 | 42 | afterEach(function () { 43 | $httpBackend.verifyNoOutstandingExpectation(); 44 | $httpBackend.verifyNoOutstandingRequest(); 45 | }); 46 | 47 | describe('provider', function () { 48 | 49 | it('should exist', function () { 50 | expect(contentfulProvider).to.be.an('object'); 51 | }); 52 | 53 | describe('#setOptions', function () { 54 | 55 | it('should be a function', function () { 56 | expect(contentfulProvider.setOptions).to.be.a('function'); 57 | }); 58 | 59 | }); 60 | 61 | describe('#setOptions()', function () { 62 | 63 | it('should return the provider', function () { 64 | expect(contentfulProvider.setOptions()).to.equal(contentfulProvider); 65 | }); 66 | 67 | }); 68 | 69 | }); 70 | 71 | describe('instance', function () { 72 | 73 | describe('#options', function () { 74 | 75 | it('should be an object', function () { 76 | expect(contentful.options).to.be.an('object'); 77 | }); 78 | 79 | it('should contain custom options passed to provider using setOptions()', function () { 80 | expect(contentful.options['default'].space).to.equal(customOptions.space); 81 | expect(contentful.options['default'].accessToken).to.equal(customOptions.accessToken); 82 | expect(contentful.options['default'].secure).to.equal(customOptions.secure); 83 | }); 84 | 85 | }); 86 | 87 | describe('#_$http', function () { 88 | 89 | it('should be an object', function () { 90 | expect(contentful._$http).to.be.a('function'); 91 | }); 92 | 93 | it('should equal $http by default', function () { 94 | expect(contentful._$http).to.equal($http); 95 | }); 96 | 97 | }); 98 | 99 | describe('#_$q', function () { 100 | 101 | it('should be an object', function () { 102 | expect(contentful._$q).to.be.a('function'); 103 | }); 104 | 105 | it('should equal $q by default', function () { 106 | expect(contentful._$q).to.equal($q); 107 | }); 108 | 109 | }); 110 | 111 | 112 | describe('#asset', function () { 113 | 114 | it('should be a function', function () { 115 | expect(contentful.asset).to.be.a('function'); 116 | }); 117 | 118 | }); 119 | 120 | describe('#asset(id)', function () { 121 | 122 | it('should perform a request to the correct API endpoint', function (done) { 123 | var id = 'customId'; 124 | $httpBackend 125 | .expectGET(expectedHost + '/spaces/dummySpace/assets/' + id + '?access_token=dummyAccessToken') 126 | .respond(200, ''); 127 | contentful.asset(id).then( 128 | function () { 129 | done(); 130 | }, 131 | function () { 132 | done(); 133 | } 134 | ); 135 | $httpBackend.flush(); 136 | }); 137 | 138 | }); 139 | 140 | describe('#assets', function () { 141 | 142 | it('should be a function', function () { 143 | expect(contentful.assets).to.be.a('function'); 144 | }); 145 | 146 | }); 147 | 148 | describe('#assets()', function () { 149 | 150 | it('should perform a request to the correct API endpoint', function (done) { 151 | $httpBackend 152 | .expectGET(expectedHost + '/spaces/dummySpace/assets?access_token=dummyAccessToken') 153 | .respond(200, ''); 154 | contentful.assets().then( 155 | function () { 156 | done(); 157 | }, 158 | function () { 159 | done(); 160 | } 161 | ); 162 | $httpBackend.flush(); 163 | }); 164 | 165 | }); 166 | 167 | describe('#contentType', function () { 168 | 169 | it('should be a function', function () { 170 | expect(contentful.contentType).to.be.a('function'); 171 | }); 172 | 173 | }); 174 | 175 | describe('#contentType(id)', function () { 176 | 177 | it('should perform a request to the correct API endpoint', function (done) { 178 | var id = 'customId'; 179 | $httpBackend 180 | .expectGET(expectedHost + '/spaces/dummySpace/content_types/' + id + '?access_token=dummyAccessToken') 181 | .respond(200, ''); 182 | contentful.contentType(id).then( 183 | function () { 184 | done(); 185 | }, 186 | function () { 187 | done(); 188 | } 189 | ); 190 | $httpBackend.flush(); 191 | }); 192 | 193 | }); 194 | 195 | describe('#contentTypes', function () { 196 | 197 | it('should be a function', function () { 198 | expect(contentful.contentTypes).to.be.a('function'); 199 | }); 200 | 201 | }); 202 | 203 | describe('#contentTypes()', function () { 204 | 205 | it('should perform a request to the correct API endpoint', function (done) { 206 | $httpBackend 207 | .expectGET(expectedHost + '/spaces/dummySpace/content_types?access_token=dummyAccessToken') 208 | .respond(200, ''); 209 | contentful.contentTypes().then( 210 | function () { 211 | done(); 212 | }, 213 | function () { 214 | done(); 215 | } 216 | ); 217 | $httpBackend.flush(); 218 | }); 219 | 220 | }); 221 | 222 | describe('#entry', function () { 223 | 224 | it('should be a function', function () { 225 | expect(contentful.entry).to.be.a('function'); 226 | }); 227 | 228 | }); 229 | 230 | describe('#entry(id)', function () { 231 | 232 | it('should perform a request to the correct API endpoint', function (done) { 233 | var id = 'customId'; 234 | $httpBackend 235 | .expectGET(expectedHost + '/spaces/dummySpace/entries/' + id + '?access_token=dummyAccessToken') 236 | .respond(200, ''); 237 | contentful.entry(id).then( 238 | function () { 239 | done(); 240 | }, 241 | function () { 242 | done(); 243 | } 244 | ); 245 | $httpBackend.flush(); 246 | }); 247 | 248 | }); 249 | 250 | describe('#entries', function () { 251 | 252 | it('should be a function', function () { 253 | expect(contentful.entries).to.be.a('function'); 254 | }); 255 | 256 | }); 257 | 258 | describe('#entries()', function () { 259 | 260 | it('should perform a request to the correct API endpoint', function (done) { 261 | $httpBackend 262 | .expectGET(expectedHost + '/spaces/dummySpace/entries?access_token=dummyAccessToken') 263 | .respond(200, ''); 264 | contentful.entries().then( 265 | function () { 266 | done(); 267 | }, 268 | function () { 269 | done(); 270 | } 271 | ); 272 | $httpBackend.flush(); 273 | }); 274 | 275 | }); 276 | 277 | describe('#space', function () { 278 | 279 | it('should be a function', function () { 280 | expect(contentful.space).to.be.a('function'); 281 | }); 282 | 283 | }); 284 | 285 | describe('#space()', function () { 286 | 287 | it('should perform a request to the correct API endpoint', function (done) { 288 | $httpBackend 289 | .expectGET(expectedHost + '/spaces/dummySpace?access_token=dummyAccessToken') 290 | .respond(200, ''); 291 | contentful.space().then( 292 | function () { 293 | done(); 294 | }, 295 | function () { 296 | done(); 297 | } 298 | ); 299 | $httpBackend.flush(); 300 | }); 301 | 302 | }); 303 | 304 | }); 305 | 306 | }); 307 | --------------------------------------------------------------------------------