├── .gitignore
├── LICENSE
├── README.md
├── async-filter.js
├── bower.json
├── karma.conf.js
├── package.json
├── src
└── async-filter.js
└── test
└── async-filter.spec.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Carl Vuorinen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Angular1 async filter
2 |
3 | > Angular2 async pipe implemented as Angular 1 filter to handle Promises & RxJS Observables
4 |
5 | The async filter takes a Promise or Observable as input and subscribes to the input automatically, eventually returning the emitted value(s) similarly as with Angular2 Async pipe.
6 | Works with RxJS Observables even without https://github.com/Reactive-Extensions/rx.angular.js
7 |
8 | ## Install
9 |
10 | Install from npm:
11 |
12 | ```
13 | npm install angular1-async-filter --save
14 | ```
15 |
16 | And bundle with Browserify, Webpack etc.
17 |
18 |
19 | Or install with bower:
20 |
21 | ```
22 | bower install angular1-async-filter --save
23 | ```
24 |
25 | And add script to html:
26 |
27 | ```html
28 |
29 | ```
30 |
31 | Add Angular module dependency. For example:
32 |
33 | ```js
34 | angular.module('myApp', ['asyncFilter']);
35 | ```
36 |
37 | ## Usage
38 |
39 | Basic usage:
40 |
41 | ```html
42 |
Value: {{ promiseOrObservable | async }}
43 | ```
44 |
45 | Works with any directive that takes an expression, like `ng-if`, `ng-show` and `ng-repeat` etc.
46 |
47 | ```html
48 |
51 | ```
52 |
53 | Angular $q and $http promises automatically trigger digest cycle when they resolve so all views will get updated.
54 | For compatibility with RxJS Observables, as well as other Promise or Observable implementations, you can provide the current scope as a parameter to the filter, which will then automatically trigger digest cycle whenever a new value is emitted:
55 |
56 | ```html
57 |
58 | Value: {{ observable | async:this }}
59 |
60 | ```
61 |
62 | If you are using the `safeApply` operator from https://github.com/Reactive-Extensions/rx.angular.js for example, then `this` is not needed.
63 |
64 | Live example: http://jsbin.com/qoxaqo/edit?html,js,output
65 |
66 | ## License
67 |
68 | MIT
69 |
--------------------------------------------------------------------------------
/async-filter.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | (function (angular) {
4 | function asyncFilter() {
5 | var values = {};
6 | var subscriptions = {};
7 |
8 | function async(input, scope) {
9 | // Make sure we have an Observable or a Promise
10 | if (!input || !(input.subscribe || input.then)) {
11 | return input;
12 | }
13 |
14 | var inputId = objectId(input);
15 | if (!(inputId in subscriptions)) {
16 | var subscriptionStrategy = input.subscribe && input.subscribe.bind(input) || input.success && input.success.bind(input) // To make it work with HttpPromise
17 | || input.then.bind(input);
18 |
19 | subscriptions[inputId] = subscriptionStrategy(function (value) {
20 | values[inputId] = value;
21 |
22 | if (scope && scope.$applyAsync) {
23 | scope.$applyAsync(); // Automatic safe apply, if scope provided
24 | }
25 | });
26 |
27 | if (scope && scope.$on) {
28 | // Clean up subscription and its last value when the scope is destroyed.
29 | scope.$on('$destroy', function () {
30 | var sub = subscriptions[inputId];
31 | if (sub) {
32 | sub.unsubscribe && sub.unsubscribe();
33 | sub.dispose && sub.dispose();
34 | }
35 | delete subscriptions[inputId];
36 | delete values[inputId];
37 | });
38 | }
39 | }
40 |
41 | return values[inputId];
42 | };
43 |
44 | // Need a way to tell the input objects apart from each other (so we only subscribe to them once)
45 | var nextObjectID = 0;
46 | function objectId(obj) {
47 | if (!obj.hasOwnProperty('__asyncFilterObjectID__')) {
48 | obj.__asyncFilterObjectID__ = ++nextObjectID;
49 | }
50 |
51 | return obj.__asyncFilterObjectID__;
52 | }
53 |
54 | // So that Angular does not cache the return value
55 | async.$stateful = true;
56 |
57 | return async;
58 | };
59 |
60 | angular.module('asyncFilter', []).filter('async', asyncFilter);
61 | })(angular);
62 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular1-async-filter",
3 | "description": "Angular2 async pipe implemented as Angular 1 filter to handle promises & RxJS observables",
4 | "main": "async-filter.js",
5 | "authors": [
6 | "Carl Vuorinen"
7 | ],
8 | "license": "MIT",
9 | "keywords": [
10 | "angular",
11 | "async",
12 | "filter",
13 | "promise",
14 | "observable",
15 | "rxjs"
16 | ],
17 | "homepage": "https://github.com/cvuorinen/angular1-async-filter",
18 | "moduleType": [],
19 | "ignore": [
20 | "**/.*",
21 | "node_modules",
22 | "bower_components",
23 | "test",
24 | "tests"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration
2 | // Generated on Wed Jun 15 2016 23:16:37 GMT+0300 (EEST)
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: ['jasmine'],
14 |
15 |
16 | // list of files / patterns to load in the browser
17 | files: [
18 | 'node_modules/phantomjs-polyfill/bind-polyfill.js',
19 | 'node_modules/angular/angular.js',
20 | 'node_modules/angular-mocks/angular-mocks.js',
21 | 'async-filter.js',
22 | 'test/**/*.spec.js'
23 | ],
24 |
25 |
26 | // list of files to exclude
27 | exclude: [
28 | ],
29 |
30 |
31 | // preprocess matching files before serving them to the browser
32 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
33 | preprocessors: {
34 | },
35 |
36 |
37 | // test results reporter to use
38 | // possible values: 'dots', 'progress'
39 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
40 | reporters: ['progress'],
41 |
42 |
43 | // web server port
44 | port: 9876,
45 |
46 |
47 | // enable / disable colors in the output (reporters and logs)
48 | colors: true,
49 |
50 |
51 | // level of logging
52 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
53 | logLevel: config.LOG_INFO,
54 |
55 |
56 | // enable / disable watching file and executing tests whenever any file changes
57 | autoWatch: true,
58 |
59 |
60 | // start these browsers
61 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
62 | browsers: ['PhantomJS'],
63 |
64 |
65 | // Continuous Integration mode
66 | // if true, Karma captures browsers, runs the tests and exits
67 | singleRun: false,
68 |
69 | // Concurrency level
70 | // how many browser should be started simultaneous
71 | concurrency: Infinity
72 | })
73 | }
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular1-async-filter",
3 | "version": "1.1.0",
4 | "description": "Angular2 async pipe implemented as Angular 1 filter to handle promises & RxJS observables",
5 | "main": "async-filter.js",
6 | "scripts": {
7 | "build": "babel src/async-filter.js -o async-filter.js --presets 'es2015'",
8 | "test": "./node_modules/karma/bin/karma start --single-run"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/cvuorinen/angular1-async-filter.git"
13 | },
14 | "keywords": [
15 | "angular",
16 | "async",
17 | "filter",
18 | "promise",
19 | "observable",
20 | "rxjs"
21 | ],
22 | "author": "Carl Vuorinen",
23 | "license": "MIT",
24 | "bugs": {
25 | "url": "https://github.com/cvuorinen/angular1-async-filter/issues"
26 | },
27 | "homepage": "https://github.com/cvuorinen/angular1-async-filter#readme",
28 | "devDependencies": {
29 | "angular": "^1.5.7",
30 | "angular-mocks": "^1.5.7",
31 | "babel-cli": "^6.5.1",
32 | "babel-preset-es2015": "^6.5.0",
33 | "jasmine": "^2.4.1",
34 | "karma": "^0.13.22",
35 | "karma-jasmine": "^1.0.2",
36 | "karma-phantomjs-launcher": "^1.0.0",
37 | "phantomjs-polyfill": "0.0.2",
38 | "phantomjs-prebuilt": "^2.1.7"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/async-filter.js:
--------------------------------------------------------------------------------
1 | (function (angular) {
2 | function asyncFilter() {
3 | const values = {};
4 | const subscriptions = {};
5 |
6 | function async(input, scope) {
7 | // Make sure we have an Observable or a Promise
8 | if (!input || !(input.subscribe || input.then)) {
9 | return input;
10 | }
11 |
12 | const inputId = objectId(input);
13 | if (!(inputId in subscriptions)) {
14 | const subscriptionStrategy = input.subscribe && input.subscribe.bind(input)
15 | || input.success && input.success.bind(input) // To make it work with HttpPromise
16 | || input.then.bind(input);
17 |
18 | subscriptions[inputId] = subscriptionStrategy(value => {
19 | values[inputId] = value;
20 |
21 | if (scope && scope.$applyAsync) {
22 | scope.$applyAsync(); // Automatic safe apply, if scope provided
23 | }
24 | });
25 |
26 | if (scope && scope.$on) {
27 | // Clean up subscription and its last value when the scope is destroyed.
28 | scope.$on('$destroy', () => {
29 | const sub = subscriptions[inputId];
30 | if (sub) {
31 | sub.unsubscribe && sub.unsubscribe();
32 | sub.dispose && sub.dispose();
33 | }
34 | delete subscriptions[inputId];
35 | delete values[inputId];
36 | });
37 | }
38 | }
39 |
40 | return values[inputId];
41 | };
42 |
43 | // Need a way to tell the input objects apart from each other (so we only subscribe to them once)
44 | let nextObjectID = 0;
45 | function objectId(obj) {
46 | if (!obj.hasOwnProperty('__asyncFilterObjectID__')) {
47 | obj.__asyncFilterObjectID__ = ++nextObjectID;
48 | }
49 |
50 | return obj.__asyncFilterObjectID__;
51 | }
52 |
53 | // So that Angular does not cache the return value
54 | async.$stateful = true;
55 |
56 | return async;
57 | };
58 |
59 | angular
60 | .module('asyncFilter', [])
61 | .filter('async', asyncFilter);
62 |
63 | })(angular);
64 |
--------------------------------------------------------------------------------
/test/async-filter.spec.js:
--------------------------------------------------------------------------------
1 | describe('Filter: async', function () {
2 | var asyncFilter,
3 | unresolvedPromise = function() {
4 | return { then: function() {} };
5 | },
6 | resolvedPromise = function(value) {
7 | return { then: function(callback) { callback(value); } };
8 | },
9 | promiseMock = function() {
10 | var callback;
11 | return {
12 | then: function(callbackFn) { callback = callbackFn; },
13 | resolve: function (value) { callback(value); }
14 | };
15 | },
16 | observableMock = function(subscription) {
17 | var callback;
18 | return {
19 | subscribe: function(callbackFn) {
20 | callback = callbackFn;
21 |
22 | return subscription;
23 | },
24 | next: function (value) { callback(value); }
25 | };
26 | },
27 | subscriptionMock = function(unsubscribeFunctionName) {
28 | var subscription = {};
29 | subscription[unsubscribeFunctionName] = function() {};
30 |
31 | return subscription;
32 | };
33 |
34 | beforeEach(module('asyncFilter'));
35 | beforeEach(inject(function ($filter) {
36 | asyncFilter = $filter('async');
37 | }));
38 |
39 | it('should return null on null input', function() {
40 | expect(asyncFilter(null)).toEqual(null);
41 | });
42 |
43 | it('should return input when it is not Promise or Observable', function() {
44 | expect(asyncFilter("foo")).toEqual("foo");
45 | });
46 |
47 | it('should return undefined when promise is not resolved', function() {
48 | expect(asyncFilter(unresolvedPromise())).toEqual(undefined);
49 | });
50 |
51 | it('should return promises resolved value', function() {
52 | expect(asyncFilter(resolvedPromise(42))).toEqual(42);
53 | });
54 |
55 | it('should return undefined until promise resolved', function() {
56 | var promise = promiseMock();
57 |
58 | expect(asyncFilter(promise)).toEqual(undefined);
59 |
60 | promise.resolve(42);
61 |
62 | expect(asyncFilter(promise)).toEqual(42);
63 | });
64 |
65 | it('should return falsy value', function() {
66 | expect(asyncFilter(resolvedPromise(0))).toEqual(0);
67 | });
68 |
69 | it('should return undefined until observable emits', function() {
70 | var observable = observableMock();
71 |
72 | expect(asyncFilter(observable)).toEqual(undefined);
73 |
74 | observable.next(42);
75 |
76 | expect(asyncFilter(observable)).toEqual(42);
77 | });
78 |
79 | it('should return observables latest value', function() {
80 | var observable = observableMock();
81 | asyncFilter(observable);
82 |
83 | observable.next(42);
84 |
85 | expect(asyncFilter(observable)).toEqual(42);
86 |
87 | observable.next("foo");
88 |
89 | expect(asyncFilter(observable)).toEqual("foo");
90 | });
91 |
92 | it('should only subscribe once', function() {
93 | var observable = observableMock();
94 | spyOn(observable, 'subscribe');
95 |
96 | asyncFilter(observable);
97 | asyncFilter(observable);
98 | asyncFilter(observable);
99 |
100 | expect(observable.subscribe).toHaveBeenCalled();
101 | expect(observable.subscribe.calls.count()).toEqual(1);
102 | });
103 |
104 | it('should call $applyAsync on each value if scope provided', function() {
105 | var observable = observableMock();
106 | var scope = {
107 | $applyAsync: jasmine.createSpy('scope.$applyAsync')
108 | };
109 |
110 | expect(asyncFilter(observable, scope)).toEqual(undefined);
111 | expect(scope.$applyAsync).not.toHaveBeenCalled();
112 |
113 | observable.next(42);
114 |
115 | expect(scope.$applyAsync).toHaveBeenCalled();
116 | expect(scope.$applyAsync.calls.count()).toEqual(1);
117 |
118 | observable.next("foo");
119 |
120 | expect(scope.$applyAsync.calls.count()).toEqual(2);
121 | });
122 |
123 | it('should listen for scope $destroy event if scope provided', function() {
124 | var observable = observableMock();
125 | var scope = {
126 | $on: jasmine.createSpy('scope.$on')
127 | };
128 |
129 | asyncFilter(observable, scope);
130 |
131 | expect(scope.$on).toHaveBeenCalledWith('$destroy', jasmine.any(Function));
132 | });
133 |
134 | it('should dispose subscription on scope $destroy event', function() {
135 | // RxJS 4 style unsubscribe
136 | var subscription = subscriptionMock('dispose');
137 | spyOn(subscription, 'dispose');
138 | var observable = observableMock(subscription);
139 | var onDestroy;
140 | var scope = {
141 | $on: function(event, callback) { onDestroy = callback; }
142 | };
143 |
144 | asyncFilter(observable, scope);
145 |
146 | expect(subscription.dispose).not.toHaveBeenCalled();
147 |
148 | onDestroy();
149 |
150 | expect(subscription.dispose).toHaveBeenCalled();
151 | });
152 |
153 | it('should unsubscribe subscription on scope $destroy event', function() {
154 | // RxJS 5 support
155 | var subscription = subscriptionMock('unsubscribe');
156 | spyOn(subscription, 'unsubscribe');
157 | var observable = observableMock(subscription);
158 | var onDestroy;
159 | var scope = {
160 | $on: function(event, callback) { onDestroy = callback; }
161 | };
162 |
163 | asyncFilter(observable, scope);
164 |
165 | expect(subscription.unsubscribe).not.toHaveBeenCalled();
166 |
167 | onDestroy();
168 |
169 | expect(subscription.unsubscribe).toHaveBeenCalled();
170 | });
171 | });
172 |
--------------------------------------------------------------------------------