UNPKG

8.1 kBJavaScriptView Raw
1/*
2 * components.js
3 *
4 * Components associate custom script functionality with a view. They can be
5 * distributed as standalone modules containing templates, scripts, and styles.
6 * They can also be used to modularize application functionality.
7 *
8 */
9
10var util = require('racer/lib/util');
11var derbyTemplates = require('derby-templates');
12var templates = derbyTemplates.templates;
13var expressions = derbyTemplates.expressions;
14var Controller = require('./Controller');
15
16exports.Component = Component;
17exports.ComponentAttribute = ComponentAttribute;
18exports.ComponentAttributeBinding = ComponentAttributeBinding;
19exports.ComponentFactory = ComponentFactory;
20exports.SingletonComponentFactory = SingletonComponentFactory;
21exports.createFactory = createFactory;
22exports.extendComponent = extendComponent;
23
24function Component(parent, context, id, scope) {
25 this.parent = parent;
26 this.context = context;
27 this.id = id;
28 this._scope = scope;
29}
30
31util.mergeInto(Component.prototype, Controller.prototype);
32
33Component.prototype.destroy = function() {
34 this.emit('destroy');
35 this.model.removeContextListeners();
36 this.model.destroy();
37 delete this.page._components[this.id];
38 var components = this.page._eventModel.object.$components;
39 if (components) delete components.object[this.id];
40};
41
42Component.prototype.get = function(viewName, unescaped) {
43 var view = this.getView(viewName);
44 return view.get(this.context, unescaped);
45};
46
47Component.prototype.getFragment = function(viewName) {
48 var view = this.getView(viewName);
49 return view.getFragment(this.context);
50};
51
52Component.prototype.getView = function(viewName) {
53 var contextView = this.context.getView();
54 return (viewName) ?
55 this.app.views.find(viewName, contextView.namespace) : contextView;
56};
57
58Component.prototype.getAttribute = function(key) {
59 var attributeContext = this.context.forAttribute(key);
60 if (!attributeContext) return;
61 var value = attributeContext.attributes[key];
62 if (value instanceof expressions.Expression) {
63 return value.get(attributeContext);
64 }
65 return value && expressions.renderValue(value, attributeContext);
66};
67
68Component.prototype.setAttribute = function(key, value) {
69 this.context.parent.attributes[key] = value;
70};
71
72Component.prototype.setNullAttribute = function(key, value) {
73 var attributes = this.context.parent.attributes;
74 if (attributes[key] == null) attributes[key] = value;
75};
76
77function ComponentAttribute(expression, model, key) {
78 this.expression = expression;
79 this.model = model;
80 this.key = key;
81}
82ComponentAttribute.prototype.update = function(context, binding) {
83 var value = this.expression.get(context);
84 binding.condition = value;
85 this.model.setDiff(this.key, value);
86};
87function ComponentAttributeBinding(expression, model, key, context) {
88 this.template = new ComponentAttribute(expression, model, key);
89 this.context = context;
90 this.condition = expression.get(context);
91}
92ComponentAttributeBinding.prototype = Object.create(templates.Binding.prototype);
93ComponentAttributeBinding.prototype.constructor = ComponentAttributeBinding;
94
95function setModelAttributes(context, model) {
96 var attributes = context.parent.attributes;
97 if (!attributes) return;
98 // Set attribute values on component model
99 for (var key in attributes) {
100 var value = attributes[key];
101 setModelAttribute(context, model, key, value);
102 }
103}
104
105function setModelAttribute(context, model, key, value) {
106 // If an attribute is an Expression, set its current value in the model
107 // and keep it up to date. When it is a resolvable path, use a Racer ref,
108 // which makes it a two-way binding. Otherwise, set to the current value
109 // and create a binding that will set the value in the model as the
110 // expression's dependencies change.
111 if (value instanceof expressions.Expression) {
112 var segments = value.pathSegments(context);
113 if (segments) {
114 model.root.ref(model._at + '.' + key, segments.join('.'), {updateIndices: true});
115 } else {
116 var binding = new ComponentAttributeBinding(value, model, key, context);
117 context.addBinding(binding);
118 model.set(key, binding.condition);
119 }
120 return;
121 }
122
123 // If an attribute is a Template, set a template object in the model.
124 // Eagerly rendering a template can cause excessive rendering when the
125 // developer wants to pass in a complex chunk of HTML, and if we were to
126 // set a string in the model that represents the template value, we'd lose
127 // the ability to use the value in the component's template, since HTML
128 // would be escaped and we'd lose the ability to create proper bindings.
129 //
130 // This may be of surprise to developers, since it may not be intuitive
131 // whether a passed in value will produce an expression or a template. To
132 // get the rendered value consistently, the component's getAttribute(key)
133 // method may be used to get the value that would be rendered.
134 if (value instanceof templates.Template) {
135 var template = new templates.ContextClosure(value, context);
136 model.set(key, template);
137 return;
138 }
139
140 // For all other value types, set the passed in value directly. Passed in
141 // values will only be set initially, so model paths should be used if
142 // bindings are desired.
143 model.set(key, value);
144}
145
146function createFactory(constructor) {
147 return (constructor.prototype.singleton) ?
148 new SingletonComponentFactory(constructor) :
149 new ComponentFactory(constructor);
150}
151
152function emitInitHooks(context, component) {
153 if (!context.initHooks) return;
154 // Run initHooks for `on` listeners immediately before init
155 for (var i = 0, len = context.initHooks.length; i < len; i++) {
156 context.initHooks[i].emit(context, component);
157 }
158}
159
160function ComponentFactory(constructor) {
161 this.constructor = constructor;
162}
163ComponentFactory.prototype.init = function(context) {
164 var component = new this.constructor();
165
166 var parent = context.controller;
167 var id = context.id();
168 var scope = ['$components', id];
169 var model = parent.model.root.eventContext(component);
170 model._at = scope.join('.');
171 model.set('id', id);
172 // Store a reference to the component's scope such that the expression
173 // getters are relative to the component
174 model.data = model.get();
175 parent.page._components[id] = component;
176
177 var componentContext = context.componentChild(component);
178 Controller.call(component, parent.app, parent.page, model);
179 Component.call(component, parent, componentContext, id, scope);
180 setModelAttributes(componentContext, model);
181
182 // Do the user-specific initialization. The component constructor should be
183 // an empty function and the actual initialization code should be done in the
184 // component's init method. This means that we don't have to rely on users
185 // properly calling the Component constructor method and avoids having to
186 // play nice with how CoffeeScript extends class constructors
187 emitInitHooks(context, component);
188 component.emit('init', component);
189 if (component.init) component.init(model);
190
191 return componentContext;
192};
193ComponentFactory.prototype.create = function(context) {
194 var component = context.controller;
195 component.emit('create', component);
196 // Call the component's create function after its view is rendered
197 if (component.create) {
198 component.create(component.model, component.dom);
199 }
200};
201
202function SingletonComponentFactory(constructor) {
203 this.constructor = constructor;
204 this.component = null;
205}
206SingletonComponentFactory.prototype.init = function(context) {
207 if (!this.component) this.component = new this.constructor();
208 return context.componentChild(this.component);
209};
210// Don't call the create method for singleton components
211SingletonComponentFactory.prototype.create = function() {};
212
213function extendComponent(constructor) {
214 // Don't do anything if the constructor already extends Component
215 if (constructor.prototype instanceof Component) return;
216 // Otherwise, replace its prototype with an instance of Component
217 var oldPrototype = constructor.prototype;
218 constructor.prototype = new Component();
219 util.mergeInto(constructor.prototype, oldPrototype);
220}