Understandings: - To use uncontrolled inputs, which I prefer, the context object should not be a state or property at the level of consumers; it should not automatically re-render with every keystroke, i.e. "The React Way." We're using Web Components, [client-side validation](https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation) exists on the platform already, and live-validation is problematic for any number of reasons. - The trade-off is that it is now necessary to re-render the target page of the wizard de-novo, but that's not really as big a deal as it sounds. Lit is ready to do that... and then nothing else until we request a change-of-page. Excellent. - The top level context *must* be a state, but it's better if it's a state never actually used by the top-level context container. The debate about whether or not to make that container a dumb one (`<slot></slot>`) or to merge it with the top-level object continues; here, I've merged it with the top-level wizard object, but that object does not refer to the state variable being managed in its render pass, so changes to it do not cause a re-render of the whole wizard. The purpose of the top-level page is to manage the *steps*, not the *content of any step*. A step may change dynamically based on the content of a step, but that's the same thing as *which step*. Lesson: always know what your state is *about*. - Deep merging is a complex subject, but here it's appropriate to our needs.
121 lines
3.5 KiB
TypeScript
121 lines
3.5 KiB
TypeScript
/** Taken from: https://github.com/zellwk/javascript/tree/master
|
|
*
|
|
* We have added some typescript annotations, but this is such a rich feature with deep nesting
|
|
* we'll just have to watch it closely for any issues. So far there don't seem to be any.
|
|
*
|
|
*/
|
|
|
|
function objectType<T>(value: T) {
|
|
return Object.prototype.toString.call(value);
|
|
}
|
|
|
|
// Creates a deep clone for each value
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function cloneDescriptorValue(value: any) {
|
|
// Arrays
|
|
if (objectType(value) === "[object Array]") {
|
|
const array = [];
|
|
for (let v of value) {
|
|
v = cloneDescriptorValue(v);
|
|
array.push(v);
|
|
}
|
|
return array;
|
|
}
|
|
|
|
// Objects
|
|
if (objectType(value) === "[object Object]") {
|
|
const obj = {};
|
|
const props = Object.keys(value);
|
|
for (const prop of props) {
|
|
const descriptor = Object.getOwnPropertyDescriptor(value, prop);
|
|
if (!descriptor) {
|
|
continue;
|
|
}
|
|
|
|
if (descriptor.value) {
|
|
descriptor.value = cloneDescriptorValue(descriptor.value);
|
|
}
|
|
Object.defineProperty(obj, prop, descriptor);
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
// Other Types of Objects
|
|
if (objectType(value) === "[object Date]") {
|
|
return new Date(value.getTime());
|
|
}
|
|
|
|
if (objectType(value) === "[object Map]") {
|
|
const map = new Map();
|
|
for (const entry of value) {
|
|
map.set(entry[0], cloneDescriptorValue(entry[1]));
|
|
}
|
|
return map;
|
|
}
|
|
|
|
if (objectType(value) === "[object Set]") {
|
|
const set = new Set();
|
|
for (const entry of value.entries()) {
|
|
set.add(cloneDescriptorValue(entry[0]));
|
|
}
|
|
return set;
|
|
}
|
|
|
|
// Types we don't need to clone or cannot clone.
|
|
// Examples:
|
|
// - Primitives don't need to clone
|
|
// - Functions cannot clone
|
|
return value;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function _merge(output: Record<string, any>, input: Record<string, any>) {
|
|
const props = Object.keys(input);
|
|
|
|
for (const prop of props) {
|
|
// Prevents Prototype Pollution
|
|
if (prop === "__proto__") continue;
|
|
|
|
const descriptor = Object.getOwnPropertyDescriptor(input, prop);
|
|
if (!descriptor) {
|
|
continue;
|
|
}
|
|
|
|
const value = descriptor.value;
|
|
if (value) descriptor.value = cloneDescriptorValue(value);
|
|
|
|
// If don't have prop => Define property
|
|
// [ken@goauthentik] Using `hasOwn` is preferable over
|
|
// the basic identity test, according to Typescript.
|
|
if (!Object.hasOwn(output, prop)) {
|
|
Object.defineProperty(output, prop, descriptor);
|
|
continue;
|
|
}
|
|
|
|
// If have prop, but type is not object => Overwrite by redefining property
|
|
if (typeof output[prop] !== "object") {
|
|
Object.defineProperty(output, prop, descriptor);
|
|
continue;
|
|
}
|
|
|
|
// If have prop, but type is Object => Concat the arrays together.
|
|
if (objectType(descriptor.value) === "[object Array]") {
|
|
output[prop] = output[prop].concat(descriptor.value);
|
|
continue;
|
|
}
|
|
|
|
// If have prop, but type is Object => Merge.
|
|
_merge(output[prop], descriptor.value);
|
|
}
|
|
}
|
|
|
|
export function merge(...sources: Array<object>) {
|
|
const result = {};
|
|
for (const source of sources) {
|
|
_merge(result, source);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export default merge;
|