Commit 804e138c authored by Ferran Recio Calderó's avatar Ferran Recio Calderó Committed by Amaia
Browse files

MDL-71209 core: reactive parent registration and updates

Now a reactive component could inherit the reactive instance from the
parent DOM element. This way components are more reusable. Apart, some
new state updates have been added. To the previous create, update and
delete, now the update message could provide also put and override,
making the state update message more REST alike and simplifying the
backend returns processing.
parent 1464843a
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
......@@ -40,7 +40,7 @@ export default class {
* property in case the mustache wants to reuse the same component logic but with a different interface.
*
* @typedef {object} descriptor
* @property {Reactive} reactive mandatory reactive module to register in
* @property {Reactive} reactive an optional reactive module to register in
* @property {DOMElement} element all components needs an element to anchor events
* @property {object} [selectors] an optional object to override query selectors
*/
......@@ -63,11 +63,6 @@ export default class {
throw Error(`Reactive components needs a main DOM element to dispatch events`);
}
if (descriptor.reactive === undefined) {
throw Error(`Reactive components needs a reactive module to work with`);
}
this.reactive = descriptor.reactive;
this.element = descriptor.element;
// Variable to track event listeners.
......@@ -88,8 +83,31 @@ export default class {
this.addSelectors(descriptor.selectors);
}
// Register the component.
this.reactive.registerComponent(this);
// Register into a reactive instance.
if (descriptor.reactive === undefined) {
// Ask parent components for registration.
this.element.dispatchEvent(new CustomEvent(
'core/reactive:requestRegistration',
{
bubbles: true,
detail: {component: this},
}
));
} else {
this.reactive = descriptor.reactive;
this.reactive.registerComponent(this);
// Add a listener to register child components.
this.addEventListener(
this.element,
'core/reactive:requestRegistration',
(event) => {
if (event?.detail?.component) {
event.stopPropagation();
this.registerChildComponent(event.detail.component);
}
}
);
}
}
/**
......@@ -403,4 +421,14 @@ export default class {
}
));
}
/**
* Register a child component into the reactive instance.
*
* @param {self} component the component to register.
*/
registerChildComponent(component) {
component.reactive = this.reactive;
this.reactive.registerComponent(component);
}
}
......@@ -24,6 +24,10 @@
import log from 'core/log';
import StateManager from 'core/local/reactive/statemanager';
import Pending from 'core/pending';
// Count the number of pending operations done to ensure we have a unique id for each one.
let pendingCount = 0;
/**
* Set up general reactive class to create a single state application with components.
......@@ -72,6 +76,10 @@ export default class {
throw new Error(`Reactivity event required`);
}
if (description.name !== undefined) {
this.name = description.name;
}
// Each reactive instance has its own element anchor to propagate state changes internally.
// By default the module will create a fake DOM element to target custom events but
// if all reactive components is constrait to a single element, this can be passed as
......@@ -94,6 +102,9 @@ export default class {
// Register the event to alert watchers when specific state change happens.
this.target.addEventListener(this.eventName, this.callWatchersHandler.bind(this));
// Add a pending operation waiting for the initial state.
this.pendingState = new Pending(`core/reactive:registerInstance${pendingCount++}`);
// Set initial state if we already have it.
if (description.state !== undefined) {
this.setInitialState(description.state);
......@@ -126,6 +137,7 @@ export default class {
* @param {object} stateData the initial state data.
*/
setInitialState(stateData) {
this.pendingState.resolve();
this.stateManager.setInitialState(stateData);
}
......@@ -225,6 +237,9 @@ export default class {
return component;
}
// Components are fully registered only when the state ready promise is resolved.
const pendingPromise = new Pending(`core/reactive:registerComponent${pendingCount++}`);
// Keep track of the event listeners.
let listeners = [];
......@@ -262,8 +277,13 @@ export default class {
// is loaded. For those cases we have a state promise to handle this specific state change.
if (component.stateReady !== undefined) {
this.getInitialStatePromise()
.then(component.stateReady.bind(component))
.then(state => {
component.stateReady(state);
pendingPromise.resolve();
return true;
})
.catch(reason => {
pendingPromise.resolve();
log.error(`Initial state in ${componentName} rejected due to: ${reason}`);
log.error(reason);
});
......@@ -328,12 +348,17 @@ export default class {
if (this.mutations[actionName] === undefined) {
throw new Error(`Unkown ${actionName} mutation`);
}
const pendingPromise = new Pending(`core/reactive:${actionName}${pendingCount++}`);
const mutationFunction = this.mutations[actionName];
try {
await mutationFunction.apply(this.mutations, [this.stateManager, ...params]);
pendingPromise.resolve();
} catch (error) {
// Ensure the state is locked.
this.stateManager.setReadOnly(true);
pendingPromise.resolve();
throw error;
}
}
......
......@@ -105,6 +105,8 @@ export default class StateManager {
"create": this.defaultCreate.bind(this),
"update": this.defaultUpdate.bind(this),
"delete": this.defaultDelete.bind(this),
"put": this.defaultPut.bind(this),
"override": this.defaultOverride.bind(this),
};
// The state_loaded event is special because it only happens one but all components
......@@ -324,6 +326,66 @@ export default class StateManager {
}
}
/**
* Process a put state message.
*
* @param {Object} stateManager the state manager
* @param {String} updateName the state element to update
* @param {Object} fields the new data
*/
defaultPut(stateManager, updateName, fields) {
// Get the current value.
let current = stateManager.get(updateName, fields.id);
if (current) {
// Update attributes.
for (const [fieldName, fieldValue] of Object.entries(fields)) {
current[fieldName] = fieldValue;
}
} else {
// Create new object.
let state = stateManager.state;
if (state[updateName] instanceof StateMap) {
state[updateName].add(fields);
return;
}
state[updateName] = fields;
}
}
/**
* Process an override state message.
*
* @param {Object} stateManager the state manager
* @param {String} updateName the state element to update
* @param {Object} fields the new data
*/
defaultOverride(stateManager, updateName, fields) {
// Get the current value.
let current = stateManager.get(updateName, fields.id);
if (current) {
// Remove any unnecessary fields.
for (const [fieldName] of Object.entries(current)) {
if (fields[fieldName] === undefined) {
delete current[fieldName];
}
}
// Update field.
for (const [fieldName, fieldValue] of Object.entries(fields)) {
current[fieldName] = fieldValue;
}
} else {
// Create the element if not exists.
let state = stateManager.state;
if (state[updateName] instanceof StateMap) {
state[updateName].add(fields);
return;
}
state[updateName] = fields;
}
}
/**
* Get an element from the state or form an alternative state object.
*
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment