UNPKG

9.98 kBJavaScriptView Raw
1import { dispatch } from "d3-dispatch";
2import { assign, isArray, isFunction, isObject, isString, pop } from "d3-let";
3import viewModel from "../model/main";
4import dashify from "../utils/dashify";
5import dataAttributes from "../utils/data";
6import map from "../utils/map";
7import maybeJson from "../utils/maybeJson";
8import sel from "../utils/sel";
9import asSelect from "../utils/select";
10import Cache from "./cache";
11import createDirective from "./directive";
12import "./selection";
13import base from "./transition";
14
15export const protoView = {
16 doMount(el) {
17 return asView(this, el);
18 }
19};
20
21// prototype for both views and components
22const protoComponent = {
23 //
24 // hooks
25 render() {},
26 childrenMounted() {},
27 mounted() {},
28 destroy() {},
29 //
30 // Mount the component into an element
31 // If this component is already mounted, or it is mounting, it does nothing
32 mount(el, data) {
33 if (mounted(this)) return;
34 const sel = asSelect(el);
35 el = sel.node();
36 if (!el) {
37 this.logWarn(
38 `element not defined, pass an identifier or an HTMLElement object`
39 );
40 return Promise.resolve(this);
41 }
42 // set the owner document
43 this.ownerDocument = el.ownerDocument;
44 //
45 const directives = sel.directives(this);
46
47 let props = assign(dataAttributes(directives.attrs), sel.datum(), data),
48 extra = maybeJson(pop(props, "props")),
49 value,
50 model,
51 parentModel;
52
53 if (isObject(extra)) props = assign(extra, props);
54 else if (this.parent && this.parent.props[extra])
55 props = assign({}, this.parent.props[extra], props);
56
57 // fire mount event
58 this.events.call("mount", undefined, this, el, props);
59
60 // pick parent model & props
61 if (this.parent) {
62 //
63 parentModel = this.parent.model;
64 model = pop(props, "model");
65 if (model && !model.$child) model = parentModel[model];
66 if (!model) model = parentModel.$new();
67 else model = model.$child();
68 //
69 // Get props from parent view
70 Object.keys(this.props).forEach(key => {
71 value = this.parent.props[props[key]];
72 if (value === undefined) {
73 value = maybeJson(props[key]);
74 if (value !== undefined) props[key] = value;
75 // default value if available
76 else if (this.props[key] !== undefined) props[key] = this.props[key];
77 } else props[key] = value;
78 });
79 //
80 // get props object from parent if props is defined
81 if (isString(extra)) props = assign({}, this.parent.props[extra], props);
82 } else {
83 props = assign(this.props, props);
84 model = viewModel();
85 }
86
87 // add reactive model properties
88 Object.keys(this.model).forEach(key => {
89 value = pop(props, key);
90 if (value !== undefined) {
91 if (isString(value) && parentModel && parentModel.$isReactive(value))
92 model.$connect(key, value, parentModel);
93 else model.$set(key, maybeJson(value));
94 } else if (model[key] === undefined) model.$set(key, this.model[key]);
95 });
96 this.model = bindView(this, model);
97 //
98 // create the new element from the render function
99 if (!props.id) props.id = model.uid;
100 this.props = props;
101 //
102 return this.doMount(el);
103 },
104
105 createElement(tag, props) {
106 const doc = this.ownerDocument || document;
107 const sel = this.select(doc.createElement(tag));
108 if (props) {
109 sel.attr("id", this.props.id);
110 if (this.props.class) sel.classed(this.props.class, true);
111 }
112 return sel;
113 },
114
115 doMount(el) {
116 let newEl;
117 try {
118 newEl = this.render(el);
119 } catch (error) {
120 newEl = Promise.reject(error);
121 }
122 if (!newEl || !newEl.then) newEl = Promise.resolve(newEl);
123 return newEl
124 .then(element => compile(this, el, element))
125 .catch(exc => error(this, el, exc));
126 },
127
128 use(plugin) {
129 if (isObject(plugin)) plugin.install(this);
130 else plugin(this);
131 return this;
132 },
133
134 addComponent(name, obj) {
135 name = dashify(name);
136 var component = createComponent(name, obj);
137 this.components.set(name, component);
138 return component;
139 },
140
141 addDirective(name, obj) {
142 name = dashify(name);
143 var directive = createDirective(obj);
144 this.directives.set(name, directive);
145 return directive;
146 }
147};
148
149// factory of View and Component constructors
150export const createComponent = (name, o, coreDirectives, coreComponents) => {
151 if (isFunction(o)) o = { render: o };
152
153 var obj = assign({}, o),
154 classComponents = extendComponents(new Map(), pop(obj, "components")),
155 classDirectives = extendDirectives(new Map(), pop(obj, "directives")),
156 model = pop(obj, "model"),
157 props = pop(obj, "props");
158
159 function Component(options) {
160 var parent = pop(options, "parent"),
161 components = map(parent ? parent.components : coreComponents),
162 directives = map(parent ? parent.directives : coreDirectives),
163 events = parent
164 ? parent.events
165 : dispatch(
166 "message",
167 "created",
168 "mount",
169 "mounted",
170 "error",
171 "directive-refresh"
172 ),
173 cache = parent ? parent.cache : new Cache();
174
175 classComponents.forEach((comp, key) => {
176 components.set(key, comp);
177 });
178 classDirectives.forEach((comp, key) => {
179 directives.set(key, comp);
180 });
181 extendComponents(components, pop(options, "components"));
182 extendDirectives(directives, pop(options, "directives"));
183
184 Object.defineProperties(this, {
185 name: {
186 get() {
187 return name;
188 }
189 },
190 components: {
191 get() {
192 return components;
193 }
194 },
195 directives: {
196 get() {
197 return directives;
198 }
199 },
200 parent: {
201 get() {
202 return parent;
203 }
204 },
205 root: {
206 get() {
207 return parent ? parent.root : this;
208 }
209 },
210 cache: {
211 get() {
212 return cache;
213 }
214 },
215 uid: {
216 get() {
217 return this.model.uid;
218 }
219 },
220 events: {
221 get() {
222 return events;
223 }
224 }
225 });
226 this.props = asObject(props, pop(options, "props"));
227 this.model = asObject(model, pop(options, "model"));
228 this.events.call("created", undefined, this);
229 }
230
231 Component.prototype = assign({}, base, protoComponent, obj);
232
233 function component(options) {
234 return new Component(options);
235 }
236
237 component.prototype = Component.prototype;
238
239 return component;
240};
241
242// Used by both Component and view
243
244export const extendComponents = (container, components) => {
245 map(components).forEach((obj, key) => {
246 key = dashify(key);
247 container.set(key, createComponent(key, obj, protoComponent));
248 });
249 return container;
250};
251
252export const extendDirectives = (container, directives) => {
253 map(directives).forEach((obj, key) => {
254 key = dashify(key);
255 container.set(key, createDirective(obj));
256 });
257 return container;
258};
259
260//
261// Finalise the binding between the view and the model
262// inject the model into the view element
263// call the mounted hook and can return a Promise
264export const asView = (vm, element) => {
265 Object.defineProperty(sel(vm), "el", {
266 get: function() {
267 return element;
268 }
269 });
270 // Apply model to element and mount
271 return vm
272 .select(element)
273 .view(vm)
274 .mount()
275 .then(() => vmMounted(vm));
276};
277
278export const mounted = vm => {
279 if (vm.isMounted === undefined) {
280 vm.isMounted = false;
281 return false;
282 } else if (vm.isMounted) {
283 vm.logWarn(`component already mounted`);
284 } else {
285 vm.isMounted = true;
286 // invoke mounted component hook
287 vm.mounted();
288 // invoke the view mounted events
289 vm.events.call("mounted", undefined, vm);
290 }
291 return true;
292};
293
294// Internals
295
296//
297// Component/View mounted
298// =========================
299//
300// This function is called when a component/view has all its children added
301const vmMounted = vm => {
302 var parent = vm.parent;
303 vm.childrenMounted();
304 if (parent && !parent.isMounted) {
305 const event = `mounted.${vm.uid}`;
306 vm.events.on(event, cm => {
307 if (cm === parent) {
308 vm.events.on(event, null);
309 mounted(vm);
310 }
311 });
312 } else mounted(vm);
313 return vm;
314};
315
316// Compile a component model
317// This function is called once a component has rendered the component element
318const compile = (cm, origEl, element) => {
319 if (isString(element)) {
320 const props = Object.keys(cm.props).length ? cm.props : null;
321 element = cm.viewElement(element, props, origEl.ownerDocument);
322 }
323 element = asSelect(element);
324 const size = element.size();
325 if (!size)
326 throw new Error(
327 "render() must return a single HTML node. It returned nothing!"
328 );
329 else if (size !== 1) cm.logWarn("render() must return a single HTML node");
330 element = element.node();
331 //
332 // mark the original element as component
333 origEl.__d3_component__ = true;
334 // Insert before the component element
335 if (origEl.parentNode) origEl.parentNode.insertBefore(element, origEl);
336 // remove the component element
337 cm.select(origEl).remove();
338 //
339 return asView(cm, element);
340};
341
342// Invoked when a component cm has failed to render
343const error = (cm, origEl, exc) => {
344 cm.logWarn(`failed to render due to the unhandled exception reported below`);
345 cm.logError(exc);
346 cm.events.call("error", undefined, cm, origEl, exc);
347 return cm;
348};
349
350const asObject = (value, opts) => {
351 if (isFunction(value)) value = value();
352 if (isArray(value))
353 value = value.reduce((o, key) => {
354 o[key] = undefined;
355 return o;
356 }, {});
357 return assign({}, value, opts);
358};
359
360const bindView = (view, model) => {
361 Object.defineProperties(model, {
362 $$view: {
363 get() {
364 return view;
365 }
366 },
367 $$name: {
368 get() {
369 return view.name;
370 }
371 },
372 props: {
373 get() {
374 return view.props;
375 }
376 }
377 });
378 return model;
379};