pageflow-react 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.jshintrc +15 -0
  4. data/CHANGELOG.md +7 -0
  5. data/Gemfile +10 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +40 -0
  8. data/Rakefile +4 -0
  9. data/app/assets/javascripts/pageflow/react.js +8975 -0
  10. data/app/assets/javascripts/pageflow/react/components.js +4 -0
  11. data/app/views/pageflow/react/_widget.html.erb +1 -0
  12. data/app/views/pageflow/react/page.html.erb +7 -0
  13. data/js/.eslintrc +33 -0
  14. data/js/.gitignore +1 -0
  15. data/js/karma.conf.js +61 -0
  16. data/js/package.json +43 -0
  17. data/js/spec/.eslintrc +8 -0
  18. data/js/spec/components/background_image_spec.js +47 -0
  19. data/js/spec/components/page_thumbnail_spec.js +213 -0
  20. data/js/spec/create_container_spec.js +82 -0
  21. data/js/spec/resolve_spec.js +3 -0
  22. data/js/spec/resolvers/backbone_model_resolver_spec.js +256 -0
  23. data/js/spec/resolvers/create_recursive_resolver_spec.js +120 -0
  24. data/js/spec/resolvers/editor_file_ids_resolver_spec.js +49 -0
  25. data/js/spec/resolvers/i18n_resolver_spec.js +20 -0
  26. data/js/spec/resolvers/object_resolver_spec.js +165 -0
  27. data/js/spec/resolvers/page_type_resolver_spec.js +23 -0
  28. data/js/spec/resolvers/seed_resolver_spec.js +128 -0
  29. data/js/spec/stub_spec.js +16 -0
  30. data/js/spec/support/render_component.js +7 -0
  31. data/js/src/components/background_image.jsx +60 -0
  32. data/js/src/components/lazy_background_image.jsx +23 -0
  33. data/js/src/components/lazy_loaded_page_thumbnail.jsx +19 -0
  34. data/js/src/components/page_background.jsx +11 -0
  35. data/js/src/components/page_background_image.jsx +13 -0
  36. data/js/src/components/page_content.jsx +51 -0
  37. data/js/src/components/page_header.jsx +15 -0
  38. data/js/src/components/page_link.jsx +35 -0
  39. data/js/src/components/page_shadow.jsx +19 -0
  40. data/js/src/components/page_text.jsx +17 -0
  41. data/js/src/components/page_thumbnail.jsx +86 -0
  42. data/js/src/components/page_wrapper.jsx +11 -0
  43. data/js/src/components/scroller.js +43 -0
  44. data/js/src/create_container.jsx +53 -0
  45. data/js/src/create_page.jsx +38 -0
  46. data/js/src/create_page_component.jsx +45 -0
  47. data/js/src/create_page_type.js +57 -0
  48. data/js/src/create_resolver_root.jsx +21 -0
  49. data/js/src/create_widget.jsx +3 -0
  50. data/js/src/create_widget_type.js +12 -0
  51. data/js/src/index.js +69 -0
  52. data/js/src/mutate.js +17 -0
  53. data/js/src/mutations/mutation.js +5 -0
  54. data/js/src/mutations/update_page_link_mutation.js +30 -0
  55. data/js/src/mutations/update_page_mutation.js +19 -0
  56. data/js/src/resolve.js +45 -0
  57. data/js/src/resolvers/backbone_model_resolver.js +118 -0
  58. data/js/src/resolvers/create_recursive_resolver.js +20 -0
  59. data/js/src/resolvers/current_parent_page_resolver.js +38 -0
  60. data/js/src/resolvers/editor_chapter_resolver.js +10 -0
  61. data/js/src/resolvers/editor_file_ids_resolver.js +30 -0
  62. data/js/src/resolvers/editor_page_resolver.js +11 -0
  63. data/js/src/resolvers/i18n_resolver.js +11 -0
  64. data/js/src/resolvers/object_resolver.js +58 -0
  65. data/js/src/resolvers/page_type_resolver.js +12 -0
  66. data/js/src/resolvers/resolver.js +16 -0
  67. data/js/src/resolvers/seed_chapter_resolver.js +10 -0
  68. data/js/src/resolvers/seed_file_ids_resolver.js +11 -0
  69. data/js/src/resolvers/seed_page_resolver.js +11 -0
  70. data/js/src/resolvers/seed_resolver.js +75 -0
  71. data/js/src/utils/camelize.js +29 -0
  72. data/js/webpack.config.js +31 -0
  73. data/lib/pageflow-react.rb +13 -0
  74. data/lib/pageflow/react/engine.rb +15 -0
  75. data/lib/pageflow/react/page_type.rb +16 -0
  76. data/lib/pageflow/react/version.rb +5 -0
  77. data/lib/pageflow/react/widget_type.rb +21 -0
  78. data/pageflow-react.gemspec +29 -0
  79. metadata +205 -0
@@ -0,0 +1,3 @@
1
+ describe('resolve', () => {
2
+
3
+ });
@@ -0,0 +1,256 @@
1
+ import BackboneModelResolver from 'resolvers/backbone_model_resolver';
2
+
3
+ import Backbone from 'backbone';
4
+
5
+ import sinon from 'sinon';
6
+
7
+ describe('BackboneModelResolver', () => {
8
+ it('gets props from model attributes referenced by id', () => {
9
+ var collection = new Backbone.Collection([{id: 1, title: 'Some title'}]);
10
+ var modelResolver = new BackboneModelResolver({
11
+ collection: () => collection,
12
+ attributesForProps: ['id', 'title'],
13
+ property: 'modelId'
14
+ });
15
+
16
+ var result = modelResolver.get({modelId: 1});
17
+
18
+ expect(result).to.deep.eq({id: 1, title: 'Some title'})
19
+ });
20
+
21
+ it('can map attribute names', () => {
22
+ var collection = new Backbone.Collection([{id: 1, template: 'background_image'}]);
23
+ var modelResolver = new BackboneModelResolver({
24
+ collection: () => collection,
25
+ attributesForProps: ['id', ['type', 'template']],
26
+ property: 'modelId'
27
+ });
28
+
29
+ var result = modelResolver.get({modelId: 1});
30
+
31
+ expect(result).to.deep.eq({id: 1, type: 'background_image'})
32
+ });
33
+
34
+ it('camelizes attribute names', () => {
35
+ var collection = new Backbone.Collection([{id: 1, image_id: 2}]);
36
+ var modelResolver = new BackboneModelResolver({
37
+ collection: () => collection,
38
+ attributesForProps: ['id', 'image_id'],
39
+ property: 'modelId'
40
+ });
41
+
42
+ var result = modelResolver.get({modelId: 1});
43
+
44
+ expect(result).to.deep.eq({id: 1, imageId: 2})
45
+ });
46
+
47
+ it('resolves to null if property is missing', () => {
48
+ var collection = new Backbone.Collection();
49
+ var modelResolver = new BackboneModelResolver({
50
+ collection: () => collection,
51
+ property: 'modelId'
52
+ });
53
+
54
+ var result = modelResolver.get({});
55
+
56
+ expect(result).to.deep.eq(null)
57
+ });
58
+
59
+ it('resolves to null if model cannot be found', () => {
60
+ var collection = new Backbone.Collection();
61
+ var modelResolver = new BackboneModelResolver({
62
+ collection: () => collection,
63
+ property: 'modelId'
64
+ });
65
+
66
+ var result = modelResolver.get({modelId: 1000});
67
+
68
+ expect(result).to.deep.eq(null)
69
+ });
70
+
71
+ it('can use custom id attribute', () => {
72
+ var collection = new Backbone.Collection([{id: 1, permaId: 100, title: 'Some title'}]);
73
+ var modelResolver = new BackboneModelResolver({
74
+ collection: () => collection,
75
+ idAttribute: 'permaId',
76
+ attributesForProps: ['id', 'title'],
77
+ property: 'modelPermaId'
78
+ });
79
+
80
+ var result = modelResolver.get({modelPermaId: 100});
81
+
82
+ expect(result).to.deep.eq({id: 1, title: 'Some title'})
83
+ });
84
+
85
+ it('gets props from new model after property value changes', () => {
86
+ var collection = new Backbone.Collection([
87
+ {id: 1, title: 'Some title'},
88
+ {id: 2, title: 'Some other title'}
89
+ ]);
90
+ var modelResolver = new BackboneModelResolver({
91
+ collection: () => collection,
92
+ attributesForProps: ['id', 'title'],
93
+ property: 'modelId'
94
+ });
95
+
96
+ modelResolver.get({modelId: 1});
97
+ var result = modelResolver.get({modelId: 2});
98
+
99
+ expect(result).to.deep.eq({id: 2, title: 'Some other title'})
100
+ });
101
+
102
+ it('invokes callback when prop attribute changes', () => {
103
+ var collection = new Backbone.Collection([{id: 1, title: 'Some title'}]);
104
+ var callback = sinon.spy();
105
+ var modelResolver = new BackboneModelResolver({
106
+ collection: () => collection,
107
+ attributesForProps: ['id', 'title'],
108
+ property: 'modelId'
109
+ }, callback);
110
+
111
+ modelResolver.get({modelId: 1});
112
+ collection.first().set('title', 'Changed title');
113
+
114
+ expect(callback).to.have.been.called;
115
+ });
116
+
117
+ it('does not invoke callback when other attributes changes', () => {
118
+ var collection = new Backbone.Collection([{id: 1, title: 'Some title'}]);
119
+ var callback = sinon.spy();
120
+ var modelResolver = new BackboneModelResolver({
121
+ collection: () => collection,
122
+ attributesForProps: ['id', 'title'],
123
+ property: 'modelId'
124
+ }, callback);
125
+
126
+ modelResolver.get({modelId: 1});
127
+ collection.first().set('text', 'Changed text');
128
+
129
+ expect(callback).not.to.have.been.called;
130
+ });
131
+
132
+ it('starts listening to new model on prop change', () => {
133
+ var collection = new Backbone.Collection([
134
+ {id: 1, title: 'Some title'},
135
+ {id: 2, title: 'Some other title'}
136
+ ]);
137
+ var callback = sinon.spy();
138
+ var modelResolver = new BackboneModelResolver({
139
+ collection: () => collection,
140
+ attributesForProps: ['id', 'title'],
141
+ property: 'modelId'
142
+ }, callback);
143
+
144
+ modelResolver.get({modelId: 1});
145
+ modelResolver.get({modelId: 2});
146
+ collection.get(2).set('title', 'Changed text');
147
+
148
+ expect(callback).to.have.been.called;
149
+ });
150
+
151
+ it('stops listening to model on prop change', () => {
152
+ var collection = new Backbone.Collection([
153
+ {id: 1, title: 'Some title'},
154
+ {id: 2, title: 'Some other title'}
155
+ ]);
156
+ var callback = sinon.spy();
157
+ var modelResolver = new BackboneModelResolver({
158
+ collection: () => collection,
159
+ attributesForProps: ['id', 'title'],
160
+ property: 'modelId'
161
+ }, callback);
162
+
163
+ modelResolver.get({modelId: 1});
164
+ modelResolver.get({modelId: 2});
165
+ collection.get(1).set('title', 'Changed text');
166
+
167
+ expect(callback).not.to.have.been.called;
168
+ });
169
+
170
+ it('stops listening to model on dispose', () => {
171
+ var collection = new Backbone.Collection([{id: 1, title: 'Some title'}]);
172
+ var callback = sinon.spy();
173
+ var modelResolver = new BackboneModelResolver({
174
+ collection: () => collection,
175
+ attributesForProps: ['id', 'title'],
176
+ property: 'modelId'
177
+ }, callback);
178
+
179
+ modelResolver.get({modelId: 1});
180
+ modelResolver.dispose();
181
+ collection.first().set('title', 'Changed title');
182
+
183
+ expect(callback).not.to.have.been.called;
184
+ });
185
+
186
+ context('with includeConfiguration option', () => {
187
+ it('includes attributes from a nested configuration model', () => {
188
+ var collection = new Backbone.Collection([{id: 1}]);
189
+ collection.first().configuration = new Backbone.Model({text: 'Some text'});
190
+ var modelResolver = new BackboneModelResolver({
191
+ collection: () => collection,
192
+ includeConfiguration: true,
193
+ property: 'modelId'
194
+ });
195
+
196
+ var result = modelResolver.get({modelId: 1});
197
+
198
+ expect(result).to.deep.eq({id: 1, text: 'Some text'})
199
+ });
200
+
201
+ it('invokes callback when configuration changes', () => {
202
+ var collection = new Backbone.Collection([{id: 1}]);
203
+ collection.first().configuration = new Backbone.Model({text: 'Some text'});
204
+ var callback = sinon.spy();
205
+ var modelResolver = new BackboneModelResolver({
206
+ collection: () => collection,
207
+ includeConfiguration: true,
208
+ property: 'modelId'
209
+ }, callback);
210
+
211
+ modelResolver.get({modelId: 1});
212
+ collection.first().configuration.set('title', 'Changed title');
213
+
214
+ expect(callback).to.have.been.called;
215
+ });
216
+
217
+ it('stops listening to model configuration on prop change', () => {
218
+ var collection = new Backbone.Collection([{id: 1}, {id: 2}]);
219
+ collection.get(1).configuration = new Backbone.Model({title: 'Some text'});
220
+ collection.get(2).configuration = new Backbone.Model({title: 'Some other text'});
221
+ var callback = sinon.spy();
222
+ var modelResolver = new BackboneModelResolver({
223
+ collection: () => collection,
224
+ attributesForProps: ['id', 'title'],
225
+ includeConfiguration: true,
226
+ property: 'modelId'
227
+ }, callback);
228
+
229
+ modelResolver.get({modelId: 1});
230
+ modelResolver.get({modelId: 2});
231
+ collection.get(1).configuration.set('title', 'Changed text');
232
+
233
+ expect(callback).not.to.have.been.called;
234
+ });
235
+
236
+ it('deeply camelizes configuration attribute names', () => {
237
+ var collection = new Backbone.Collection([{id: 1}]);
238
+ collection.first().configuration = new Backbone.Model({
239
+ page_links: [
240
+ {
241
+ image_id: 1
242
+ }
243
+ ]
244
+ });
245
+ var modelResolver = new BackboneModelResolver({
246
+ collection: () => collection,
247
+ includeConfiguration: true,
248
+ property: 'modelId'
249
+ });
250
+
251
+ var result = modelResolver.get({modelId: 1});
252
+
253
+ expect(result).to.deep.eq({id: 1, pageLinks: [{imageId: 1}]})
254
+ });
255
+ });
256
+ });
@@ -0,0 +1,120 @@
1
+ import Resolver from 'resolvers/resolver';
2
+ import createRecursiveResolver from 'resolvers/create_recursive_resolver';
3
+
4
+ import sinon from 'sinon';
5
+
6
+ describe('resolver created by createRecursiveResolver', () => {
7
+ class FakeResolver extends Resolver {
8
+ constructor(options, callback) {
9
+ super(callback);
10
+
11
+ this.dispose = options.onDispose || sinon.spy();
12
+ this._triggerCallbackOnGet = options.triggerCallbackOnGet;
13
+ this._result = options.result;
14
+ }
15
+
16
+ get(props, seed) {
17
+ if (this._triggerCallbackOnGet) {
18
+ this.triggerCallback();
19
+ }
20
+
21
+ return this._result;
22
+ }
23
+
24
+ triggerCallback() {
25
+ this._handleChange();
26
+ }
27
+ }
28
+
29
+ it('returns result of decorated resolver', () => {
30
+ const RecursiveResolver = createRecursiveResolver(FakeResolver);
31
+ const resolver = new RecursiveResolver({
32
+ result: {chapterId: 5}
33
+ });
34
+
35
+ var result = resolver.get();
36
+
37
+ expect(result.chapterId).to.eq(5);
38
+ });
39
+
40
+ it('uses fragments to resolve properties in result', () => {
41
+ const RecursiveResolver = createRecursiveResolver(FakeResolver);
42
+ const resolver = new RecursiveResolver({
43
+ result: {chapterId: 5},
44
+ fragments: {
45
+ chapter: (callback) => new FakeResolver({result: 'chapter'}, callback)
46
+ }
47
+ });
48
+
49
+ var result = resolver.get();
50
+
51
+ expect(result.chapter).to.eq('chapter');
52
+ });
53
+
54
+ it('invokes callback when the nested resolver signals a change', () => {
55
+ const RecursiveResolver = createRecursiveResolver(FakeResolver);
56
+ const callback = sinon.spy();
57
+ var nestedResolver;
58
+ const resolver = new RecursiveResolver({
59
+ result: {chapterId: 5},
60
+ fragments: {
61
+ chapter: (callback) => {
62
+ nestedResolver = new FakeResolver({result: 'chapter'}, callback);
63
+ return nestedResolver;
64
+ }
65
+ }
66
+ }, callback);
67
+
68
+ resolver.get();
69
+ nestedResolver.triggerCallback();
70
+
71
+ expect(callback).to.have.been.called;
72
+ });
73
+
74
+ it('invokes callback when the decorated resolver signals a change', () => {
75
+ const RecursiveResolver = createRecursiveResolver(FakeResolver);
76
+ const callback = sinon.spy();
77
+ const resolver = new RecursiveResolver({
78
+ triggerCallbackOnGet: true,
79
+ result: {chapterId: 5},
80
+ }, callback);
81
+
82
+ resolver.get();
83
+
84
+ expect(callback).to.have.been.called;
85
+ });
86
+
87
+ it('passes dispose on nested resolver', () => {
88
+ const RecursiveResolver = createRecursiveResolver(FakeResolver);
89
+ var nestedResolver;
90
+ const resolver = new RecursiveResolver({
91
+ result: {chapterId: 5},
92
+ fragments: {
93
+ chapter: (callback) => {
94
+ nestedResolver = new FakeResolver({result: 'chapter'}, callback);
95
+ return nestedResolver;
96
+ }
97
+ }
98
+ }, () => {});
99
+
100
+ resolver.get();
101
+ resolver.dispose();
102
+
103
+ expect(nestedResolver.dispose).to.have.been.called;
104
+ });
105
+
106
+ it('passes dispose to decorated resolver', () => {
107
+ const RecursiveResolver = createRecursiveResolver(FakeResolver);
108
+ var disposed = false;
109
+ const resolver = new RecursiveResolver({
110
+ triggerCallbackOnGet: true,
111
+ onDispose: () => { disposed = true; },
112
+ result: {chapterId: 5},
113
+ }, () => {});
114
+
115
+ resolver.get();
116
+ resolver.dispose();
117
+
118
+ expect(disposed).to.eq(true);
119
+ });
120
+ });
@@ -0,0 +1,49 @@
1
+ import EditorFileIdsResolver from 'resolvers/editor_file_ids_resolver';
2
+
3
+ import Backbone from 'backbone';
4
+
5
+ import sinon from 'sinon';
6
+
7
+ describe('EditorFileIdsResolver', () => {
8
+ it('gets ids of files indexed by collection name', () => {
9
+ var files = {
10
+ image_files: new Backbone.Collection([{id: 1}, {id: 3}])
11
+ };
12
+ var resolver = new EditorFileIdsResolver({
13
+ collections: () => files
14
+ });
15
+
16
+ var result = resolver.get();
17
+
18
+ expect(result).to.deep.eq({image_files: [1, 3]})
19
+ });
20
+
21
+ it('invokes callback when prop attribute changes', () => {
22
+ var files = {
23
+ image_files: new Backbone.Collection([{id: 1}])
24
+ };
25
+ var callback = sinon.spy();
26
+ new EditorFileIdsResolver({
27
+ collections: () => files
28
+ }, callback);
29
+
30
+ files.image_files.shift();
31
+
32
+ expect(callback).to.have.been.called;
33
+ });
34
+
35
+ it('stops invokes callback after dispose', () => {
36
+ var files = {
37
+ image_files: new Backbone.Collection([{id: 1}])
38
+ };
39
+ var callback = sinon.spy();
40
+ var resolver = new EditorFileIdsResolver({
41
+ collections: () => files
42
+ }, callback);
43
+
44
+ resolver.dispose();
45
+ files.image_files.shift();
46
+
47
+ expect(callback).not.to.have.been.called;
48
+ });
49
+ });
@@ -0,0 +1,20 @@
1
+ import I18nResolver from 'resolvers/i18n_resolver';
2
+
3
+ import sinon from 'sinon';
4
+
5
+ describe('I18nResolver', () => {
6
+ it('provides translate function for entry locale', () => {
7
+ window.I18n = {
8
+ t: sinon.spy()
9
+ };
10
+
11
+ var seed = {
12
+ locale: 'fr'
13
+ };
14
+ var resolver = new I18nResolver();
15
+
16
+ resolver.get({}, seed).t('some.key');
17
+
18
+ expect(I18n.t).to.have.been.calledWith('some.key', {locale: 'fr'});
19
+ });
20
+ });