asjs-express 1.2.0 → 1.4.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.
- package/README.md +360 -0
- package/bin/create-asjs-app.js +223 -0
- package/lib/asjs.js +306 -13
- package/lib/client/asjs-router.js +46 -1
- package/package.json +12 -2
- package/templates/minimal/README.md.tpl +12 -0
- package/templates/minimal/app.js.tpl +29 -0
- package/templates/minimal/gitignore.tpl +4 -0
- package/templates/minimal/package.json.tpl +15 -0
- package/templates/minimal/views/home.asjs.tpl +5 -0
- package/templates/minimal/views/layouts/main.asjs.tpl +16 -0
- package/templates/starter/README.md.tpl +12 -0
- package/templates/starter/app.js.tpl +54 -0
- package/templates/starter/gitignore.tpl +4 -0
- package/templates/starter/package.json.tpl +15 -0
- package/templates/starter/views/about.asjs.tpl +10 -0
- package/templates/starter/views/home.asjs.tpl +10 -0
- package/templates/starter/views/layouts/main.asjs.tpl +16 -0
package/lib/asjs.js
CHANGED
|
@@ -25,6 +25,192 @@ function normalizeNumberOption(value, fallback) {
|
|
|
25
25
|
return Number.isFinite(normalized) && normalized >= 0 ? normalized : fallback;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function delay(duration) {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
setTimeout(resolve, normalizeNumberOption(duration, 0));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function flattenClassNames(value) {
|
|
35
|
+
if (!value) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
return value.flatMap(flattenClassNames);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return String(value)
|
|
44
|
+
.split(/\s+/)
|
|
45
|
+
.map((item) => item.trim())
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function joinClassNames(...values) {
|
|
50
|
+
return [...new Set(values.flatMap(flattenClassNames))].join(' ');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function formatTimestamp(value = new Date(), options = {}) {
|
|
54
|
+
const settings = options && typeof options === 'object' ? options : {};
|
|
55
|
+
const locale = settings.locale || 'en-US';
|
|
56
|
+
const formatOptions = settings.formatOptions && typeof settings.formatOptions === 'object'
|
|
57
|
+
? settings.formatOptions
|
|
58
|
+
: {
|
|
59
|
+
dateStyle: 'medium',
|
|
60
|
+
timeStyle: 'short'
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return new Intl.DateTimeFormat(locale, formatOptions).format(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeFieldValue(value) {
|
|
67
|
+
return String(value || '').trim();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeFields(input = {}, fields = []) {
|
|
71
|
+
const source = input && typeof input === 'object' ? input : {};
|
|
72
|
+
const list = Array.isArray(fields)
|
|
73
|
+
? fields
|
|
74
|
+
: Object.keys(fields && typeof fields === 'object' ? fields : {});
|
|
75
|
+
|
|
76
|
+
return list.reduce((result, fieldName) => {
|
|
77
|
+
result[fieldName] = normalizeFieldValue(source[fieldName]);
|
|
78
|
+
return result;
|
|
79
|
+
}, {});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resolveValidationMessage(ruleValue, fallback) {
|
|
83
|
+
if (typeof ruleValue === 'string' && ruleValue) {
|
|
84
|
+
return ruleValue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (ruleValue && typeof ruleValue === 'object' && typeof ruleValue.message === 'string' && ruleValue.message) {
|
|
88
|
+
return ruleValue.message;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return fallback;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function validateFields(values = {}, schema = {}) {
|
|
95
|
+
const input = values && typeof values === 'object' ? values : {};
|
|
96
|
+
const rules = schema && typeof schema === 'object' ? schema : {};
|
|
97
|
+
const errors = {};
|
|
98
|
+
|
|
99
|
+
Object.entries(rules).forEach(([fieldName, fieldRules]) => {
|
|
100
|
+
const config = fieldRules && typeof fieldRules === 'object' ? fieldRules : {};
|
|
101
|
+
const label = config.label || fieldName;
|
|
102
|
+
const rawValue = Object.prototype.hasOwnProperty.call(input, fieldName) ? input[fieldName] : '';
|
|
103
|
+
const value = typeof rawValue === 'string' ? rawValue : normalizeFieldValue(rawValue);
|
|
104
|
+
|
|
105
|
+
if (config.required && !value) {
|
|
106
|
+
errors[fieldName] = resolveValidationMessage(config.required, `${label} is required.`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (config.minLength) {
|
|
111
|
+
const minLength = normalizeNumberOption(
|
|
112
|
+
typeof config.minLength === 'object' ? config.minLength.value : config.minLength,
|
|
113
|
+
0
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (minLength > 0 && value.length < minLength) {
|
|
117
|
+
errors[fieldName] = resolveValidationMessage(
|
|
118
|
+
config.minLength,
|
|
119
|
+
`${label} must be at least ${minLength} characters.`
|
|
120
|
+
);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (config.pattern) {
|
|
126
|
+
const pattern = config.pattern instanceof RegExp
|
|
127
|
+
? config.pattern
|
|
128
|
+
: (config.pattern && config.pattern.value instanceof RegExp ? config.pattern.value : null);
|
|
129
|
+
|
|
130
|
+
if (pattern && value && !pattern.test(value)) {
|
|
131
|
+
errors[fieldName] = resolveValidationMessage(config.pattern, `${label} format is invalid.`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (typeof config.validate === 'function') {
|
|
137
|
+
const validationResult = config.validate(value, input);
|
|
138
|
+
|
|
139
|
+
if (typeof validationResult === 'string' && validationResult) {
|
|
140
|
+
errors[fieldName] = validationResult;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return errors;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function hasValidationErrors(errors) {
|
|
149
|
+
return Boolean(errors && typeof errors === 'object' && Object.keys(errors).length);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function renderInlineResponse(options = {}) {
|
|
153
|
+
const settings = options && typeof options === 'object' ? options : {};
|
|
154
|
+
const facts = Array.isArray(settings.facts) ? settings.facts : [];
|
|
155
|
+
const tone = settings.tone === 'error'
|
|
156
|
+
? 'is-error'
|
|
157
|
+
: (settings.tone === 'neutral' ? 'is-neutral' : 'is-success');
|
|
158
|
+
const rootClassName = joinClassNames('asjs-inline-response', tone, settings.className);
|
|
159
|
+
|
|
160
|
+
return [
|
|
161
|
+
`<div class="${escapeHtml(rootClassName)}">`,
|
|
162
|
+
settings.title ? ` <strong>${escapeHtml(settings.title)}</strong>` : '',
|
|
163
|
+
` <p>${escapeHtml(settings.message || 'The request was checked successfully.')}</p>`,
|
|
164
|
+
facts.length
|
|
165
|
+
? ' <div class="inline-response-meta">' + facts.map((item) => (`<span><strong>${escapeHtml(item.label)}</strong>${escapeHtml(item.value)}</span>`)).join('') + '</div>'
|
|
166
|
+
: '',
|
|
167
|
+
'</div>'
|
|
168
|
+
].join('');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function createPageModel(defaults = {}) {
|
|
172
|
+
const baseDefaults = defaults && typeof defaults === 'object' ? defaults : {};
|
|
173
|
+
|
|
174
|
+
return function buildPageModel(pageData = {}) {
|
|
175
|
+
const input = pageData && typeof pageData === 'object' ? pageData : {};
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
...baseDefaults,
|
|
179
|
+
...input
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function createPageRoute(pageHandlerFactory, viewName, options, pageFactory) {
|
|
185
|
+
const hasOptionsObject = options && typeof options === 'object' && !Array.isArray(options);
|
|
186
|
+
const settings = hasOptionsObject ? options : {};
|
|
187
|
+
const resolver = hasOptionsObject ? pageFactory : options;
|
|
188
|
+
const buildPage = typeof settings.buildPage === 'function'
|
|
189
|
+
? settings.buildPage
|
|
190
|
+
: createPageModel(settings.pageDefaults || {});
|
|
191
|
+
const renderStateOptions = settings.renderState === false
|
|
192
|
+
? false
|
|
193
|
+
: (settings.renderState && typeof settings.renderState === 'object' ? settings.renderState : null);
|
|
194
|
+
|
|
195
|
+
return pageHandlerFactory(viewName, async (req, res, next) => {
|
|
196
|
+
const renderState = renderStateOptions ? await createRenderState(req, renderStateOptions) : {};
|
|
197
|
+
const resolvedPageData = await Promise.resolve(
|
|
198
|
+
typeof resolver === 'function' ? resolver(req, res, next) : resolver
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
return buildPage({
|
|
202
|
+
...renderState,
|
|
203
|
+
...(resolvedPageData && typeof resolvedPageData === 'object' ? resolvedPageData : {})
|
|
204
|
+
}, {
|
|
205
|
+
next,
|
|
206
|
+
renderState,
|
|
207
|
+
req,
|
|
208
|
+
res,
|
|
209
|
+
viewName
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
28
214
|
function normalizeAssetMountPath(value) {
|
|
29
215
|
const rawValue = value === undefined || value === null || value === ''
|
|
30
216
|
? '_asjs'
|
|
@@ -289,6 +475,42 @@ function normalizeSiteCtaOptions(value, fallback = null) {
|
|
|
289
475
|
return defaults;
|
|
290
476
|
}
|
|
291
477
|
|
|
478
|
+
function normalizeLoadingBarOptions(value, fallback = { enabled: true, className: '' }) {
|
|
479
|
+
const base = fallback && typeof fallback === 'object'
|
|
480
|
+
? {
|
|
481
|
+
enabled: fallback.enabled !== false,
|
|
482
|
+
className: typeof fallback.className === 'string' ? fallback.className : ''
|
|
483
|
+
}
|
|
484
|
+
: { enabled: true, className: '' };
|
|
485
|
+
|
|
486
|
+
if (value === undefined) {
|
|
487
|
+
return base;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (typeof value === 'boolean') {
|
|
491
|
+
return {
|
|
492
|
+
...base,
|
|
493
|
+
enabled: value
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (typeof value === 'string') {
|
|
498
|
+
return {
|
|
499
|
+
enabled: true,
|
|
500
|
+
className: value.trim()
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (value && typeof value === 'object') {
|
|
505
|
+
return {
|
|
506
|
+
enabled: value.enabled !== false,
|
|
507
|
+
className: typeof value.className === 'string' ? value.className.trim() : base.className
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return base;
|
|
512
|
+
}
|
|
513
|
+
|
|
292
514
|
function resolveHeaderState(config, sourceLocals = {}, options = {}) {
|
|
293
515
|
const locals = sourceLocals && typeof sourceLocals === 'object' ? sourceLocals : {};
|
|
294
516
|
const input = options && typeof options === 'object' ? options : {};
|
|
@@ -571,6 +793,7 @@ function renderBodyAttrs(asjs, extraAttributes = {}) {
|
|
|
571
793
|
'data-asjs-prefetch': asjs.prefetch ? 'true' : 'false',
|
|
572
794
|
'data-asjs-prefetch-ttl': asjs.prefetchTtl,
|
|
573
795
|
'data-asjs-loading-bar': asjs.loadingBar ? 'true' : 'false',
|
|
796
|
+
'data-asjs-loading-class': asjs.loadingBarClassName || undefined,
|
|
574
797
|
'data-asjs-forms': asjs.forms && asjs.forms.enabled ? 'true' : 'false',
|
|
575
798
|
'data-asjs-form-selector': asjs.forms ? asjs.forms.selector : undefined,
|
|
576
799
|
'data-asjs-form-mode': asjs.forms ? asjs.forms.mode : undefined,
|
|
@@ -583,16 +806,29 @@ function renderBodyAttrs(asjs, extraAttributes = {}) {
|
|
|
583
806
|
}
|
|
584
807
|
|
|
585
808
|
function renderViewAttrs(asjs, extraAttributes = {}) {
|
|
809
|
+
const attributes = extraAttributes && typeof extraAttributes === 'object' ? { ...extraAttributes } : {};
|
|
810
|
+
|
|
586
811
|
return serializeHtmlAttributes({
|
|
812
|
+
...attributes,
|
|
813
|
+
class: joinClassNames('asjs-view', asjs.transition.className, attributes.class),
|
|
587
814
|
'data-asjs-view': true,
|
|
588
815
|
'data-asjs-transition': asjs.transition.enabled ? asjs.transition.name : 'none',
|
|
589
816
|
'data-asjs-transition-duration': asjs.transition.duration,
|
|
590
|
-
...extraAttributes
|
|
591
817
|
});
|
|
592
818
|
}
|
|
593
819
|
|
|
594
|
-
function renderProgressMarkup() {
|
|
595
|
-
|
|
820
|
+
function renderProgressMarkup(asjs = {}, options = {}) {
|
|
821
|
+
const settings = options && typeof options === 'object' ? options : {};
|
|
822
|
+
const customClassName = joinClassNames(asjs.loadingBarClassName, settings.className);
|
|
823
|
+
const rootClassName = joinClassNames('asjs-progress', customClassName);
|
|
824
|
+
const barClassName = joinClassNames('asjs-progress-bar', settings.barClassName);
|
|
825
|
+
|
|
826
|
+
return `<div${serializeHtmlAttributes({
|
|
827
|
+
class: rootClassName,
|
|
828
|
+
'data-asjs-progress': true,
|
|
829
|
+
'data-asjs-progress-class': customClassName || undefined,
|
|
830
|
+
'aria-hidden': 'true'
|
|
831
|
+
})}><span${serializeHtmlAttributes({ class: barClassName })}></span></div>`;
|
|
596
832
|
}
|
|
597
833
|
|
|
598
834
|
function createAsjsViewModel(inputAsjs, config, overrides = {}) {
|
|
@@ -601,6 +837,18 @@ function createAsjsViewModel(inputAsjs, config, overrides = {}) {
|
|
|
601
837
|
const transition = Object.prototype.hasOwnProperty.call(overrides, 'transition')
|
|
602
838
|
? normalizeTransitionOptions(overrides.transition, config.transitions)
|
|
603
839
|
: normalizeTransitionOptions(current.transition, config.transitions);
|
|
840
|
+
const loadingBarState = normalizeLoadingBarOptions(
|
|
841
|
+
Object.prototype.hasOwnProperty.call(current, 'loadingBarClassName')
|
|
842
|
+
? {
|
|
843
|
+
enabled: Object.prototype.hasOwnProperty.call(current, 'loadingBar') ? current.loadingBar : config.loadingBar,
|
|
844
|
+
className: current.loadingBarClassName
|
|
845
|
+
}
|
|
846
|
+
: current.loadingBar,
|
|
847
|
+
{
|
|
848
|
+
enabled: config.loadingBar,
|
|
849
|
+
className: config.loadingBarClassName
|
|
850
|
+
}
|
|
851
|
+
);
|
|
604
852
|
|
|
605
853
|
const viewModel = {
|
|
606
854
|
...current,
|
|
@@ -612,9 +860,8 @@ function createAsjsViewModel(inputAsjs, config, overrides = {}) {
|
|
|
612
860
|
prefetchTtl: Object.prototype.hasOwnProperty.call(current, 'prefetchTtl')
|
|
613
861
|
? normalizeNumberOption(current.prefetchTtl, config.prefetchTtl)
|
|
614
862
|
: config.prefetchTtl,
|
|
615
|
-
loadingBar:
|
|
616
|
-
|
|
617
|
-
: config.loadingBar,
|
|
863
|
+
loadingBar: loadingBarState.enabled,
|
|
864
|
+
loadingBarClassName: loadingBarState.className,
|
|
618
865
|
forms: Object.prototype.hasOwnProperty.call(current, 'forms')
|
|
619
866
|
? normalizeFormOptions(current.forms, config.forms)
|
|
620
867
|
: normalizeFormOptions(undefined, config.forms),
|
|
@@ -629,7 +876,7 @@ function createAsjsViewModel(inputAsjs, config, overrides = {}) {
|
|
|
629
876
|
viewModel.bodyAttrs = (extraAttributes) => renderBodyAttrs(viewModel, extraAttributes);
|
|
630
877
|
viewModel.viewAttrs = (extraAttributes) => renderViewAttrs(viewModel, extraAttributes);
|
|
631
878
|
viewModel.formAttrs = (options) => renderFormAttrs(viewModel, options);
|
|
632
|
-
viewModel.progressMarkup = () => renderProgressMarkup();
|
|
879
|
+
viewModel.progressMarkup = (options) => renderProgressMarkup(viewModel, options);
|
|
633
880
|
viewModel.header = (options) => renderHeaderMarkup(resolveHeaderState(config, {}, options));
|
|
634
881
|
|
|
635
882
|
return viewModel;
|
|
@@ -908,19 +1155,19 @@ function normalizeTransitionOptions(value, fallback = null) {
|
|
|
908
1155
|
if (value === undefined) {
|
|
909
1156
|
return fallback
|
|
910
1157
|
? { ...fallback }
|
|
911
|
-
: { enabled: false, name: 'none', duration: 0 };
|
|
1158
|
+
: { enabled: false, name: 'none', duration: 0, className: '' };
|
|
912
1159
|
}
|
|
913
1160
|
|
|
914
1161
|
if (value === false || value === null || value === 'none') {
|
|
915
|
-
return { enabled: false, name: 'none', duration: 0 };
|
|
1162
|
+
return { enabled: false, name: 'none', duration: 0, className: '' };
|
|
916
1163
|
}
|
|
917
1164
|
|
|
918
1165
|
if (value === true) {
|
|
919
|
-
return { enabled: true, name: 'fade', duration: 260 };
|
|
1166
|
+
return { enabled: true, name: 'fade', duration: 260, className: '' };
|
|
920
1167
|
}
|
|
921
1168
|
|
|
922
1169
|
if (typeof value === 'string') {
|
|
923
|
-
return { enabled: true, name: value, duration: 260 };
|
|
1170
|
+
return { enabled: true, name: value, duration: 260, className: '' };
|
|
924
1171
|
}
|
|
925
1172
|
|
|
926
1173
|
if (typeof value === 'object') {
|
|
@@ -931,6 +1178,7 @@ function normalizeTransitionOptions(value, fallback = null) {
|
|
|
931
1178
|
|
|
932
1179
|
return {
|
|
933
1180
|
enabled,
|
|
1181
|
+
className: value.className ? String(value.className).trim() : '',
|
|
934
1182
|
name: enabled ? String(value.name || 'fade') : 'none',
|
|
935
1183
|
duration: enabled ? duration : 0
|
|
936
1184
|
};
|
|
@@ -938,7 +1186,7 @@ function normalizeTransitionOptions(value, fallback = null) {
|
|
|
938
1186
|
|
|
939
1187
|
return fallback
|
|
940
1188
|
? { ...fallback }
|
|
941
|
-
: { enabled: false, name: 'none', duration: 0 };
|
|
1189
|
+
: { enabled: false, name: 'none', duration: 0, className: '' };
|
|
942
1190
|
}
|
|
943
1191
|
|
|
944
1192
|
function resolveTemplatePath(name, currentFile, viewsDir, extension) {
|
|
@@ -1162,7 +1410,8 @@ function createAsjsConfig(options = {}) {
|
|
|
1162
1410
|
transitions: normalizeTransitionOptions(options.transitions),
|
|
1163
1411
|
prefetch: normalizeBooleanOption(options.prefetch, true),
|
|
1164
1412
|
prefetchTtl: normalizeNumberOption(options.prefetchTtl, 30000),
|
|
1165
|
-
loadingBar:
|
|
1413
|
+
loadingBar: normalizeLoadingBarOptions(options.loadingBar).enabled,
|
|
1414
|
+
loadingBarClassName: normalizeLoadingBarOptions(options.loadingBar).className,
|
|
1166
1415
|
forms: normalizeFormOptions(
|
|
1167
1416
|
Object.prototype.hasOwnProperty.call(options, 'forms') ? options.forms : true
|
|
1168
1417
|
),
|
|
@@ -1612,6 +1861,30 @@ function buildPageLocals(req, config, pageData = {}) {
|
|
|
1612
1861
|
};
|
|
1613
1862
|
}
|
|
1614
1863
|
|
|
1864
|
+
async function createRenderState(req, options = {}) {
|
|
1865
|
+
const settings = options && typeof options === 'object' ? options : {};
|
|
1866
|
+
const startedAt = Date.now();
|
|
1867
|
+
|
|
1868
|
+
await delay(settings.delay);
|
|
1869
|
+
|
|
1870
|
+
const elapsed = Date.now() - startedAt;
|
|
1871
|
+
const summary = Array.isArray(settings.summary)
|
|
1872
|
+
? settings.summary
|
|
1873
|
+
: [
|
|
1874
|
+
{ label: 'Mode', value: settings.mode || 'res.render()' },
|
|
1875
|
+
{ label: 'Prepared by', value: settings.preparedBy || 'Async route callback' },
|
|
1876
|
+
{ label: 'Ready in', value: `${elapsed} ms` },
|
|
1877
|
+
{ label: 'Route', value: req.originalUrl || req.url }
|
|
1878
|
+
];
|
|
1879
|
+
|
|
1880
|
+
return {
|
|
1881
|
+
renderLabel: settings.label || 'Server-prepared page model',
|
|
1882
|
+
renderNarrative: settings.narrative || 'This page used an async ASJS callback so data work finished before res.render() produced the final HTML.',
|
|
1883
|
+
renderSummary: summary,
|
|
1884
|
+
renderTimestamp: formatTimestamp(new Date(), settings.timestamp || settings)
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1615
1888
|
function setupAsjs(app, options = {}) {
|
|
1616
1889
|
const config = createAsjsConfig(options);
|
|
1617
1890
|
|
|
@@ -1748,8 +2021,18 @@ function setupAsjs(app, options = {}) {
|
|
|
1748
2021
|
app,
|
|
1749
2022
|
config,
|
|
1750
2023
|
clearCache,
|
|
2024
|
+
createPageModel,
|
|
2025
|
+
createPageRoute(viewName, routeOptions, pageFactory) {
|
|
2026
|
+
return createPageRoute(page, viewName, routeOptions, pageFactory);
|
|
2027
|
+
},
|
|
2028
|
+
createRenderState,
|
|
1751
2029
|
express,
|
|
2030
|
+
formatTimestamp,
|
|
2031
|
+
hasValidationErrors,
|
|
2032
|
+
normalizeFields,
|
|
2033
|
+
normalizeFieldValue,
|
|
1752
2034
|
render,
|
|
2035
|
+
renderInlineResponse,
|
|
1753
2036
|
page,
|
|
1754
2037
|
route: page,
|
|
1755
2038
|
notFound,
|
|
@@ -1758,6 +2041,7 @@ function setupAsjs(app, options = {}) {
|
|
|
1758
2041
|
packagePaths: config.packagePaths,
|
|
1759
2042
|
extendLocals,
|
|
1760
2043
|
defineComponents,
|
|
2044
|
+
validateFields,
|
|
1761
2045
|
addHook,
|
|
1762
2046
|
use(plugin) {
|
|
1763
2047
|
applyAsjsPlugin(plugin, api);
|
|
@@ -1792,13 +2076,22 @@ function setupAsjs(app, options = {}) {
|
|
|
1792
2076
|
}
|
|
1793
2077
|
|
|
1794
2078
|
module.exports = {
|
|
2079
|
+
createPageModel,
|
|
2080
|
+
createPageRoute,
|
|
1795
2081
|
createAsjsConfig,
|
|
1796
2082
|
createAsjsEngine,
|
|
2083
|
+
createRenderState,
|
|
1797
2084
|
escapeHtml,
|
|
2085
|
+
formatTimestamp,
|
|
1798
2086
|
getAsjsPackagePaths,
|
|
2087
|
+
hasValidationErrors,
|
|
2088
|
+
normalizeFields,
|
|
2089
|
+
normalizeFieldValue,
|
|
1799
2090
|
renderClientTags,
|
|
1800
2091
|
renderDebugErrorPage,
|
|
1801
2092
|
renderHeaderMarkup,
|
|
2093
|
+
renderInlineResponse,
|
|
2094
|
+
validateFields,
|
|
1802
2095
|
validateComponentProps,
|
|
1803
2096
|
setupAsjs
|
|
1804
2097
|
};
|
|
@@ -26,6 +26,26 @@
|
|
|
26
26
|
return Number.isFinite(normalized) && normalized >= 0 ? normalized : fallback;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function flattenClassNames(value) {
|
|
30
|
+
if (!value) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
return value.reduce((result, item) => result.concat(flattenClassNames(item)), []);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return String(value)
|
|
39
|
+
.split(/\s+/)
|
|
40
|
+
.map((item) => item.trim())
|
|
41
|
+
.filter(Boolean);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function joinClassNames() {
|
|
45
|
+
const values = Array.prototype.slice.call(arguments);
|
|
46
|
+
return Array.from(new Set(values.reduce((result, value) => result.concat(flattenClassNames(value)), []))).join(' ');
|
|
47
|
+
}
|
|
48
|
+
|
|
29
49
|
function normalizePathname(value) {
|
|
30
50
|
const normalized = String(value || '/').replace(/\/+$/, '');
|
|
31
51
|
return normalized || '/';
|
|
@@ -258,6 +278,7 @@
|
|
|
258
278
|
this.prefetchEnabled = parseBooleanAttribute(root && root.dataset.asjsPrefetch, this.defaultPrefetch);
|
|
259
279
|
this.prefetchTtl = parseNumberAttribute(root && root.dataset.asjsPrefetchTtl, this.defaultPrefetchTtl);
|
|
260
280
|
this.loadingBarEnabled = parseBooleanAttribute(root && root.dataset.asjsLoadingBar, this.defaultLoadingBar);
|
|
281
|
+
this.loadingBarClassName = (root && root.dataset.asjsLoadingClass) || '';
|
|
261
282
|
this.formsEnabled = parseBooleanAttribute(root && root.dataset.asjsForms, this.defaultForms);
|
|
262
283
|
this.formSelector = (root && root.dataset.asjsFormSelector) || this.defaultFormSelector;
|
|
263
284
|
this.formMode = (root && root.dataset.asjsFormMode) || this.defaultFormMode;
|
|
@@ -265,17 +286,41 @@
|
|
|
265
286
|
this.formResetOnSuccess = parseBooleanAttribute(root && root.dataset.asjsFormResetOnSuccess, this.defaultFormResetOnSuccess);
|
|
266
287
|
this.formTarget = (root && root.dataset.asjsFormTarget) || this.defaultFormTarget;
|
|
267
288
|
this.formSwap = (root && root.dataset.asjsFormSwap) || this.defaultFormSwap;
|
|
289
|
+
this.syncProgressRootClassName();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
getProgressRootClassName() {
|
|
293
|
+
const progressClassName = this.progressRoot && this.progressRoot.dataset
|
|
294
|
+
? this.progressRoot.dataset.asjsProgressClass || ''
|
|
295
|
+
: '';
|
|
296
|
+
|
|
297
|
+
return joinClassNames('asjs-progress', this.loadingBarClassName, progressClassName);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
syncProgressRootClassName() {
|
|
301
|
+
if (!this.progressRoot) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const isActive = this.progressRoot.classList.contains('is-active');
|
|
306
|
+
this.progressRoot.className = this.getProgressRootClassName();
|
|
307
|
+
|
|
308
|
+
if (isActive) {
|
|
309
|
+
this.progressRoot.classList.add('is-active');
|
|
310
|
+
}
|
|
268
311
|
}
|
|
269
312
|
|
|
270
313
|
ensureProgressBar() {
|
|
271
314
|
if (!this.progressRoot) {
|
|
272
315
|
this.progressRoot = document.createElement('div');
|
|
273
|
-
this.progressRoot.className =
|
|
316
|
+
this.progressRoot.className = this.getProgressRootClassName();
|
|
274
317
|
this.progressRoot.dataset.asjsProgress = 'true';
|
|
275
318
|
this.progressRoot.innerHTML = '<span class="asjs-progress-bar"></span>';
|
|
276
319
|
document.body.prepend(this.progressRoot);
|
|
277
320
|
}
|
|
278
321
|
|
|
322
|
+
this.syncProgressRootClassName();
|
|
323
|
+
|
|
279
324
|
this.progressBar = this.progressRoot.querySelector('.asjs-progress-bar');
|
|
280
325
|
return Boolean(this.progressRoot && this.progressBar);
|
|
281
326
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "asjs-express",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Lightweight Express view engine with EJS-like templates, layouts, async page rendering, form enhancement, and a built-in client router.",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"asjs-express": "./bin/create-asjs-app.js",
|
|
8
|
+
"create-asjs-app": "./bin/create-asjs-app.js"
|
|
9
|
+
},
|
|
6
10
|
"author": {
|
|
7
11
|
"name": "Acarfx",
|
|
8
12
|
"url": "https://acarfx.com"
|
|
@@ -21,28 +25,34 @@
|
|
|
21
25
|
"./package.json": "./package.json"
|
|
22
26
|
},
|
|
23
27
|
"files": [
|
|
28
|
+
"bin",
|
|
24
29
|
"index.js",
|
|
25
30
|
"lib",
|
|
26
|
-
"README.md"
|
|
31
|
+
"README.md",
|
|
32
|
+
"templates"
|
|
27
33
|
],
|
|
28
34
|
"scripts": {
|
|
29
35
|
"start": "node example-express/app.js",
|
|
30
36
|
"demo": "node example-express/app.js",
|
|
31
37
|
"dev": "node example-express/app.js",
|
|
32
38
|
"example:express": "node example-express/app.js",
|
|
39
|
+
"example:minimal": "node example-minimal/app.js",
|
|
33
40
|
"pack:check": "npm pack --dry-run"
|
|
34
41
|
},
|
|
35
42
|
"keywords": [
|
|
36
43
|
"asjs",
|
|
37
44
|
"component",
|
|
45
|
+
"cli",
|
|
38
46
|
"ejs",
|
|
39
47
|
"express",
|
|
40
48
|
"express-view-engine",
|
|
41
49
|
"forms",
|
|
42
50
|
"layout",
|
|
43
51
|
"prefetch",
|
|
52
|
+
"scaffold",
|
|
44
53
|
"server-rendered",
|
|
45
54
|
"ssr",
|
|
55
|
+
"starter-template",
|
|
46
56
|
"template-engine",
|
|
47
57
|
"view-engine",
|
|
48
58
|
"spa-navigation"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const { setupAsjs } = require('asjs-express');
|
|
3
|
+
|
|
4
|
+
const app = express();
|
|
5
|
+
const port = process.env.PORT || __PORT__;
|
|
6
|
+
|
|
7
|
+
const asjs = setupAsjs(app, {
|
|
8
|
+
rootDir: __dirname,
|
|
9
|
+
defaultLayout: 'layouts/main',
|
|
10
|
+
navItems: [
|
|
11
|
+
{ href: '/', label: 'Home', activeMode: 'exact' }
|
|
12
|
+
],
|
|
13
|
+
transitions: 'fade',
|
|
14
|
+
prefetch: true,
|
|
15
|
+
loadingBar: true
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
app.get('/', asjs.page('home', {
|
|
19
|
+
title: '__APP_TITLE__',
|
|
20
|
+
headline: '__APP_TITLE__ is ready.',
|
|
21
|
+
description: 'Header, router, loading bar, and SPA-ready page transitions are already connected.',
|
|
22
|
+
nextStep: 'Add a second route whenever you need it. ASJS will keep the same internal navigation flow.'
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
app.use(asjs.errors());
|
|
26
|
+
|
|
27
|
+
app.listen(port, () => {
|
|
28
|
+
console.log(`__APP_TITLE__ running at http://localhost:${port}`);
|
|
29
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PACKAGE_NAME__",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "ASJS minimal starter generated by npx asjs-express",
|
|
6
|
+
"main": "app.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "node app.js",
|
|
9
|
+
"start": "node app.js"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"asjs-express": "^__ASJS_VERSION__",
|
|
13
|
+
"express": "__EXPRESS_VERSION__"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title><%= title %></title>
|
|
7
|
+
<%- asjs.clientTags({ preload: true, theme: true }) %>
|
|
8
|
+
</head>
|
|
9
|
+
<body<%- asjs.bodyAttrs() %>>
|
|
10
|
+
<%- asjs.progressMarkup() %>
|
|
11
|
+
<%- asjs.header() %>
|
|
12
|
+
<main class="view-frame"<%- asjs.viewAttrs() %>>
|
|
13
|
+
<%- body %>
|
|
14
|
+
</main>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|