[Design] - Overriding the question cursor service implementation when using QuML library as a Web Component
Background:
Currently, the QuML player library exports the QuestionCursor abstract class, which allow users to override the QuestionCursor implementation when using the QuML player as an angular component. It has some methods that should make an API request over HTTP outside of the QuML player:
GetQuestion (Question List API)
GetQuestions (Question List API)
Since we converted the Angular QuML player to a web component, this question cursor service implementation is bundled as part of the web component. So, now the user is not able to override the implementation of the QuestionCursor abstract class.
Problem Statement:
As user, how can I override the question cursor implementation when using the QuML player as a web component?
Solution:
As of now, there is no direct way to pass a custom service as a parameter to a web component. However, there are a few workarounds that we can do.
Approach 1: Use an event emitter callback type.
In this approach, we can use an event emitter callback type to make the API call after the web component is created. To do this, we can create a custom event type and then emit an event of that type when the getQuestion method is called.
Here is an example of how to override a custom service using an event emitter callback type:
const qumlPlayerElement = document.createElement('sunbird-quml-player');
qumlPlayerElement.addEventListener('playerEvent', (event) => {
console.log("On playerEvent", event);
});
qumlPlayerElement.addEventListener('getQuestion', (event) => { { identifier, callback }
// make API call or logic to return question data
callback(questions); // list of the questions
});
Approach 2: Use a global service on the Window object
Create a global service on the Window object.
In the useFactory provider for your QuestionService, check for the global service on the Window object. If the global service exists, then use it to instantiate the QuestionService for your component. Otherwise, use the default service.
// Create a global service on the Window object
window.customQuestionService = new My customQuestionService;
providers: [{
provide: MyService, useFactory: () => {
if (window.customQuestionService) {
return window.customQuestionService;
}
return new MyService();
}
}]
Approach 3: Use a custom element property
Create a custom element property to pass the custom service to the web component.
In the useFactory provider for your QuestionService, use the custom element property to instantiate the QuestionService for your component.
providers: [{ provide: MyService, useFactory: () => {
const customService = customElements.get('my-component').questionService;
if (customService) { return customService; } return new MyService();
}
}]
Approach 4: Use a global event bus
Use a global event bus to pass the custom service to the web component.
In the useFactory provider for your QuestionService, listen for the event on the global event bus and use it to instantiate the QuestionService for your component.
// Use the global event bus to pass the custom service to the web component
window.EventBus.emit(' QuestionService’, new QuestionService ());
providers: [{ provide: MyService, useFactory: () => {
const customService = window.EventBus.get(' QuestionService’);
if (customService) {
return customService;
}
return new MyService();
} }]
Approach 5: Use a custom event emitter callback
To inject a custom service after the web component is created, you can emit a custom event with the custom service as the payload.
// Inject a custom service
const customService = new MyCustomService();
const webComponent = document.querySelector('my-component');
webComponent.dispatchEvent(new CustomEvent('custom-service-injected', {
detail: customService
}));
connectedCallback() {
// Listen for the `custom-service-injected` event
this.addEventListener('custom-service-injected', (event: CustomEvent) => {
// Get the custom service
const customService = event.detail;
// Inject the custom service
this.myService = customService;
});
}
Additional notes
If we are using the useFactory approach, we need to make sure that the factory function is always returning a new instance of the service. This is because Angular will only inject a service once into a component.
If we are using the custom element property approach, we need to make sure that the custom element property is set before the web component is created. This is because Angular will inject the service when the web component is created.
If we are using the global event bus approach, we need to make sure that the event is emitted before the web component is created. This is because Angular will inject the service