pageflow-react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ });