Angular 2 - Use ComponentResolver to load component downloaded at runtime - dynamic

I'm trying to load a component dynamically in my Angular2 app.
By dynamically I don't just mean to insert into the DOM a component that is missing from the parent component's template; I mean to download from the server another component, because the User might not ever need it, and then insert it into the DOM.
So I followed this to understand how to load the component with Systemjs and now I see that, in RC4, we need to use this and ComponentResolver to inject things into the DOM.
So here is my code for the module-wrapper component that should put things together:
export class ModuleWrapperComponent implements OnInit {
#ViewChild("module", { read: ViewContainerRef }) target: ViewContainerRef;
#Input() isOpen: boolean;
#Input() moduleId: number;
type: string = null;
moduleRef: ComponentRef<any> = null;
private isViewInitialized: boolean = false;
constructor(private resolver: ComponentResolver) { }
private _loadModule(moduleId: number): Promise<string> {
if (!!this.type)
return Promise.resolve(this.type);
var mod = {
component: "DashboardComponent",
path: "/app/modules/dashboard.component"
};
return System
.import(mod.path)
.then(() => this.type = mod.component);
}
updateComponent() {
if (!this.isViewInitialized)
return;
if (!!this.moduleRef)
this.moduleRef.destroy();
if (this.isOpen) {
this._loadModule(this.moduleId)
.then(type => {
this.resolver.resolveComponent(type)
.then(factory => {
this.moduleRef = this.target.createComponent(factory)
// to access the created instance use
// this.compRef.instance.someProperty = 'someValue';
// this.compRef.instance.someOutput.subscribe(val => doSomething());
});
})
}
}
ngOnChanges() {
this.updateComponent();
}
ngAfterViewInit() {
this.isViewInitialized = true;
this.updateComponent();
}
ngOnDestroy() {
if (!!this.moduleRef)
this.moduleRef.destroy();
}
}
Sadly, I get this error from ComponentResolver:
Error: Cannot resolve component using 'DashboardComponent'. at new BaseException$1 (http://localhost:63815/lib/#angular/compiler//bundles/compiler.umd.js:971:27)
I reckon that loading the module in Systemjs is not enough...so how do I tell ComponentResolver that I've downloaded a new component? The official documentation is still young and lacking...
On a side note, say that my Dashboard component loads additional stuff with imports...Systemjs will handle all that automatically, won't it?
Thanks in advance!

This does not exactly answer your question.
BUT,
We had a similar requirement and were loading components dynamically using SystemJs.
It was all working fine until we started thinking about build and deployment strategies and came across Webpack
Since the value of moduleId is only known at runtime, so webpack will not be able to resolve the actual module components at compile time and will not bundle those files.
You will need to have static imports for your dynamic module components somewhere in your code if you want them to be bundled as part of your single app.js

Related

Vuex-module-decorator, modifying state inside an action

Using the vuex-module-decorator I have a authenticate action that should mutate the state.
#Action
public authenticate(email: string, password: string): Promise<Principal> {
this.principal = null;
return authenticator
.authenticate(email, password)
.then(auth => {
const principal = new Principal(auth.username);
this.context.commit('setPrincipal', principal);
return principal;
})
.catch(error => {
this.context.commit('setError', error);
return error;
});
}
// mutations for error and principal
But this fail with the following message:
Unhandled promise rejection Error: "ERR_ACTION_ACCESS_UNDEFINED: Are you trying to access this.someMutation() or this.someGetter inside an #Action?
That works only in dynamic modules.
If not dynamic use this.context.commit("mutationName", payload) and this.context.getters["getterName"]
What I don't understand is that it works well with #MutationAction and async. However I miss the return type Promise<Principal>.
#MutationAction
public async authenticate(email: string, password: string) {
this.principal = null;
try {
const auth = await authenticator.authenticate(email, password);
return { principal: new Principal(auth.username), error: null };
} catch (ex) {
const error = ex as Error;
return { principal: null, error };
}
}
--
At this time I feel blocked and would like to have some help to implement an #Action that can mutate the state and return a specific type in a Promise.
Just add rawError option to the annotation so it becomes
#Action({rawError: true})
And it display error normally. this is because the the library "vuex-module-decorators" wrap error so by doing this you will able to get a RawError that you can work with
You can vote down this answer if you would like because it isn't answering the specific question being posed. Instead, I am going to suggest that if you are using typescript, then don't use vuex. I have spent the past month trying to learn vue /vuex and typescript. The one thing I am committed to is using typescript because I am a firm believer in the benefits of using typescript. I will never use raw javascript again.
If somebody would have told me to not use vuex from the beginning, I would have saved myself 3 of the past 4 weeks. So I am here to try and share that insight with others.
The key is Vue 3's new ref implementation. It is what really changes the game for vuex and typescript. It allows us to not have to rely on vuex to automatically wrap state in a reactive. Instead, we can do that ourselves with the ref construct in vue 3. Here is a small example from my app that uses ref and a typescript class where I was expecting to use vuex in the past.
NOTE1: the one thing you lose when using this approach is vuex dev tools.
NOTE2: I might be biased as I am ported 25,000 lines of typescript (with 7000 unit tests) from Knockout.js to Vue. Knockout.js was all about providing Observables (Vue's ref) and binding. Looking back, it was kind of ahead of its time, but it didn't get the following and support.
Ok, lets create a vuex module class that doesn't use vuex. Put this in appStore.ts. To simplify it will just include the user info and the id of the club the user is logged into. A user can switch clubs so there is an action to do that.
export class AppClass {
public loaded: Ref<boolean>;
public userId: Ref<number>;
public userFirstName: Ref<string>;
public userLastName: Ref<string>;
// Getters are computed if you want to use them in components
public userName: Ref<string>;
constructor() {
this.loaded = ref(false);
initializeFromServer()
.then(info: SomeTypeWithSettingsFromServer) => {
this.userId = ref(info.userId);
this.userFirstName = ref(info.userFirstName);
this.userLastName = ref(info.userLastName);
this.userName = computed<string>(() =>
return this.userFirstName.value + ' ' + this.userLastName.value;
}
}
.catch(/* do some error handling here */);
}
private initializeFromServer(): Promise<SomeTypeWithSettingsFromServer> {
return axios.get('url').then((response) => response.data);
}
// This is a getter that you don't need to be reactive
public fullName(): string {
return this.userFirstName.value + ' ' + this.userLastName.value;
}
public switchToClub(clubId: number): Promise<any> {
return axios.post('switch url')
.then((data: clubInfo) => {
// do some processing here
}
.catch(// do some error handling here);
}
}
export appModule = new AppClass();
Then when you want to access appModule anywhere, you end up doing this:
import { appModule } from 'AppStore';
...
if (appModule.loaded.value) {
const userName = appModule.fullName();
}
or in a compositionApi based component. This is what would replace mapActions etc.
<script lang="ts">
import { defineComponent } from '#vue/composition-api';
import { appModule } from '#/store/appStore';
import footer from './footer/footer.vue';
export default defineComponent({
name: 'App',
components: { sfooter: footer },
props: {},
setup() {
return { ...appModule }
}
});
</script>
and now you can use userId, userFirstName, userName etc in your template.
Hope that helps.
I just added the computed getter. I need to test if that is really needed. It might not be needed because you might be able to just reference fullName() in your template and since fullName() references the .value variables of the other refs, fullName might become a reference itself. But I have to check that out first.
I sugest this simple solution, work fine for me 👌:
// In SomeClassComponent.vue
import { getModule } from "vuex-module-decorators";
import YourModule from "#/store/YourModule";
someMethod() {
const moduleStore = getModule(YourModule, this.$store);
moduleStore.someAction();
}
If the action has parameters, put them.
Taken from: https://github.com/championswimmer/vuex-module-decorators/issues/86#issuecomment-464027359

Angular 5: Route animations for navigating to the same route, but different parameters

In my Angular 5 application, the user may navigate to a route which uses the same route, but with different parameters. For example, they may navigate from /page/1 to /page/2.
I want this navigation to trigger the routing animation, but it doesn't. How can I cause a router animation to happen between these two routes?
(I already understand that unlike most route changes, this navigation does not destroy and create a new PageComponent. It doesn't matter to me whether or not the solution changes this behavior.)
Here's a minimal app that reproduces my issue.
This is an old question but that's it if you're still searching.
Add this code to your app.Component.ts file.
import { Router, NavigationEnd } from '#angular/router';
constructor(private _Router: Router) { }
ngOnInit() {
this._Router.routeReuseStrategy.shouldReuseRoute = function(){
return false;
};
this._Router.events.subscribe((evt) => {
if (evt instanceof NavigationEnd) {
this._Router.navigated = false;
window.scrollTo(0, 0);
}
});
}
By using this code the page is going to refresh if you clicked on the same route no matter what is the parameter you added to the route.
I hope that helps.
Update
As angular 6 is released with core updates you don't need this punch of code anymore just add the following parameter to your routs import.
onSameUrlNavigation: 'reload'
This option value set to 'ignore' by default.
Example
#NgModule({
imports: [RouterModule.forRoot(routes, { onSameUrlNavigation: 'reload'})],
exports: [RouterModule]
})
Stay up to date and happy coding.
I ended up creating a custom RouteReuseStrategy which got the job done. It's heavily based on this answer.
export class CustomReuseStrategy implements RouteReuseStrategy {
storedRouteHandles = new Map<string, DetachedRouteHandle>();
shouldDetach(route: ActivatedRouteSnapshot): boolean {
return false;
}
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
this.storedRouteHandles.set(route.routeConfig.path, handle);
}
shouldAttach(route: ActivatedRouteSnapshot): boolean {
return false;
}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
return this.storedRouteHandles.get(route.routeConfig.path);
}
// This is the important part! We reuse the route if
// the route *and its params* are the same.
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
return future.routeConfig === curr.routeConfig &&
future.params.page === curr.params.page;
}
}
Check it out on StackBlitz!

Aurelia common class/model

I'm trying to implement a globally-accessible singular class in an Aurelia project. The purposes are to (a) store singulares/states like current user ID/name/permissions, (b) load and store common data like enum lists and key-value pairs for drop-down lists across the whole app, (c) store commonly-used functions like wrappers for Http-Fetch client, (d) configure and then update i18n locale, (e) global keyboard listener for hotkeys throughout the app. Here's what I have so far:
/src/resources/components/core.js:
import 'fetch';
import { HttpClient, json } from 'aurelia-fetch-client';
import { inject } from 'aurelia-framework';
import { EventAggregator } from 'aurelia-event-aggregator';
import { BindingSignaler } from 'aurelia-templating-resources';
import { I18N } from 'aurelia-i18n';
import * as store from 'store';
#inject(EventAggregator, BindingSignaler, I18N, HttpClient)
export class Core {
constructor(eventAggregator, bindingSignaler, i18n, httpClient) {
// store local handles
this.eventAggregator = eventAggregator;
this.bindingSignaler = bindingSignaler;
this.i18n = i18n;
// initialize singulars
this.UserID = 1;
this.lang = 'es';
this.yr = 78;
this.qtr = 1;
// set up httpClient
httpClient.configure(config => {
config
.withBaseUrl('http://localhost:8080/api/v1');
});
this.httpClient = httpClient;
// listen for Ctrl+S or Ctrl+Enter and publish event
window.addEventListener("keydown", (event) => {
if (event.ctrlKey || event.metaKey) { // Ctrl + ___
if ((event.keyCode == 83) || (event.keyCode == 115) || (event.keyCode == 10) || (event.keyCode == 13)) { // Ctrl+Enter or Ctrl+S
// Save button... publish new event
event.preventDefault();
this.eventAggregator.publish('ewKeyboardShortcutSave', true);
}
if ((event.keyCode == 77) || (event.keyCode == 109)) { // Ctrl+M
// New button... publish new event
event.preventDefault();
this.eventAggregator.publish('ewKeyboardShortcutNew', true);
}
}
});
// load enumData
$.getJSON("../../locales/" + this.lang + "/enum.json", (json) => { this.enum = json; });
this.getTableKeys();
this.getEnumCats();
}
getData(url) {
// Http Fetch Client to retreive data (GET)
return this.httpClient.fetch(url)
.then(response => response.json());
}
postData(url, data, use_method = 'post') {
// Http Fetch Client to send data (POST/PUT/DELETE)
return this.httpClient.fetch(url, {
method: use_method,
body: json(data)
}).then(response => {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
});
}
getTableKeys() {
// retrieve list of table keys from database API
this.getData('/keys').then(response => {
this.keys = response;
});
}
getEnumCats() {
// retrieve list of enum cats from database API
this.getData('/enums').then(response => {
this.cats = response;
});
}
setLang(lang) {
if (lang) {
this.lang = lang;
}
// set i18n locale
this.i18n.setLocale(this.lang);
// load enumData
$.getJSON("../../locales/" + this.lang + "/enum.json", (json) => {
this.enumData = json;
});
// publish new event
this.eventAggregator.publish('ewLang', lang);
this.bindingSignaler.signal('ewLang');
}
}
Here's the /src/resources/index.js for the resources feature:
export function configure(config) {
// value converters
config.globalResources([
'./value-converters/currency-format-value-converter',
'./value-converters/number-format-value-converter',
'./value-converters/date-format-value-converter',
'./value-converters/checkbox-value-converter',
'./value-converters/keys-value-converter',
'./value-converters/enum-value-converter',
'./value-converters/table-key-value-converter'
]);
// custom elements
config.globalResources([
'./elements/enum-list',
'./elements/modal-form'
]);
// common/core components
config.globalResources([
'./components/core'
]);
}
which is in turn activated in my main.js like this:
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.feature('resources')
// .plugin('aurelia-dialog') // not working
.plugin('aurelia-validation')
.plugin('aurelia-i18n', (instance) => {
// register backend plugin
instance.i18next.use(XHR);
instance.setup({
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
lng : 'en',
ns: ['translation'],
defaultNS: 'translation',
attributes : ['t'],
fallbackLng : 'en',
debug : false
});
});
aurelia.start().then(a => a.setRoot());
}
Questions:
It's not working. I get two errors: vendor-bundle.js:3777 Uncaught TypeError: h.load is not a function and Unhandled rejection Error: Load timeout for modules: template-registry-entry!resources/components/core.html,text!resources/components/core.html. Any idea why it's trying to find a core.html when I only need the core.js component?
Is it even possible to globally inject this type of class in a way that my viewmodels don't need to inject it but can still access the properties, or do I still need to inject this file everywhere?
Is the filename core.js and the class name Core acceptable naming conventions? Is the location inside /src/resources/components a good choice? I had to create the components subfolder.
Any other suggestions for better best practices?
Question 1
When you do this:
config.globalResources([
'./components/core'
]);
Aurelia will try to load a pair of view and view-model, respectively core.js and core.html, unless if the component is declared as a "view-model only component". Like this:
import { noView } from 'aurelia-framework';
#noView
#inject(EventAggregator, BindingSignaler, I18N, HttpClient)
export class Core {
}
In the above case Aurelia won't try to load "core.html" because the component is declared with noView.
Question 2
As far as I know, you have to inject or <require> it everywhere, but the latter doesn't apply in your case, so you have to inject. You could some trickery to avoid the injecting but I would not recommend.
Question 3
The file name core.js and the class name Core are not only acceptable but the correct aurelia-way of doing this. However, I don't think that "/resources/components" is a good a location because it's not a component, not even a "resource". I would move this to another folder.
In addition, remove these lines:
config.globalResources([
'./components/core'
]);
Resources were made to be used inside views, which is not you are case.
Question 4
The file core.js seems to be a very import piece of code of your application. I would leave it inside the root folder, next to main.js. (THIS IS MY OPINION)
Also, if you need to set some specific properties in your Core object, you can instantiate it inside the main.js. Something like this:
export function configure(aurelia) {
//...
Core core = new Core(); //<--- put necessary parameters
//some default configuration
aurelia.container.registerInstance(Core, core);
aurelia.start().then(a => a.setRoot());
}
Now, you can inject the core object using the #inject decorator and all classe will have the same instance of Core. More information at http://aurelia.io/hub.html#/doc/article/aurelia/dependency-injection/latest/dependency-injection-basics/1
Hope this helps!

Shadow DOM and testing it via Jasmine

I have a webcomponent that creates a shadow DOM and adds some html to its shadowRoot.
class SomeThing extends HTMLElement {
attachedCallback () {
this.el = this.createShadowRoot();
this.render();
}
render () {
this.el.innerHTML = '<h1>Hello</h1>';
}
}
export default SomeThing;
And I am compiling it with the help of webpack and its babel-core and babel-preset-es2015 plugins.
Also I am using Karma and Jasmine to write my Unit Test. This is what it looks like.
describe('some-thing', function () {
var someElement;
beforeEach(function () {
someElement = document.createElement('some-thing');
});
it('created element should match string representation', function () {
var expectedEl = '<some-thing></some-thing>';
var wrapper = document.createElement('div');
wrapper.appendChild(someElement);
expect(wrapper.innerHTML).toBe(expectedEl);
});
it('created element should have shadow root', function () {
var wrapper = document.createElement('div');
wrapper.appendChild(someElement);
expect(wrapper.querySelector('some-thing').shadowRoot).not.toBeNull();
})
});
I want to see if there is something in the shadowRoot of my element, and want to write test cases for the HTML and events created inside the shadowRoot. But the second test is failing. It is not able to add shadowRoot to the some-element DOM.
If anyone can help me out, that would be helpful.
I am also uploading the full test working project on Github. You can access it via this link https://github.com/prateekjadhwani/unit-tests-for-shadow-dom-webcomponents
Thanks in advance
I had a similar problem testing a web component but in my case I am using lit-element from polymer/lit-element. Lit-element provides life cycle hooks, template rendering using lit-html library (documentation).
So this is my problem and how I solved. I noticed that the component was added and the class executed constructor and I had access to public methods using:
const element = document.querySelector('my-component-name')
element.METHOD();
element.VARIABLE
But it never reached the hook firstUpdated, so I thought the problem was the speed the test executes vs the speed component is created. So I used the promised provided by lit-element API (updateComplete):
Note: I use mocha/chai instead of Jasmine
class MyComponent extends LitElement {
render() {
return html`<h1>Hello</h1>`
}
}
customElements.define('my-component', TodoApp);
let element;
describe('main', () => {
beforeEach(() => {
element = document.createElement("my-component");
document.querySelector('body').appendChild(element);
});
describe('test', () => {
it('Checks that header tag was added to shadowRoot', (done) => {
(async () => {
const res = await element.updateComplete;
const header = element.shadowRoot.querySelector('h1');
assert.notEqual(header, null);
done();
})();
});
});
});
So, my advice is create a promise and resolve it when the render function is executed, use the promise to sync the creation of the component with tests.
I am using this repository to test concepts
https://github.com/correju/polymer-playground

IE not calling lifecycle hooks or filling #Input until change detected?

I'm using beta.0 because this outstanding bug prevents angular 2 from working in IE in beta.1 and beta.2.
Relevant code from SearchBar.ts
#Component({
selector : 'search-bar',
templateUrl: 'views/searchbar.html'
})
export class SearchBar {
private history: SearchHistoryEntry[] = [];
#Output() onHistory = new EventEmitter();
constructor() {
this.history = JSON.parse(localStorage.getItem('SearchHistory')) || [];
}
ngOnInit() {
// The constructor doesn't have #Outputs initialized yet. Emit from this
// life cycle hook instead to be sure they're received on the other side
debugger;
this.onHistory.emit(this.history);
}
}
Relevant code from home.html
<search-bar (onHistory)="SearchBarHistory($event)"></search-bar>
Relevant code from home.ts
SearchBarHistory(history: SearchHistoryEntry[]) {
debugger;
this.history = history;
}
In Chrome this works just fine. The SearchBar's constructor correctly reads from localStorage, in ngOnInit it emits to my Home component who receives it, it's stored locally and the UI bindings tied to history update to show the information as it all should.
In IE 11 this does not work. ngOnInit won't run until I click inside my search bar. It seems that any #Input or lifecycle hook (specifically I've tested ngOnInit, ngAfterContentInit, and ngAfterViewInit and they all behave the same) doesn't run until the component's change detection is triggered. If I refresh the page then it runs exactly like Chrome where no interaction is required for #Inputs or lifecycle hooks to be called and my history goes through and gets bound like it should.
I think this is a bug of the beta but in the mean time is there anything I can do to make it work the first time without an interaction or page refresh?
I am having the same issue I tried it to resolve by forcing detectChanges like:
import {Injectable,ApplicationRef, NgZone} from '#angular/core';
#Injectable()
export class IeHackService {
constructor(
private _appRef: ApplicationRef,
private _zone: NgZone) {}
private isIe() {
let ua = window.navigator.userAgent;
let msie = ua.indexOf('MSIE ');
if (msie > 0 || !!navigator.userAgent.match(/Trident.*rv\:11\./)) // If Internet Explorer, return version number
return true;
return false;
}
onComponentLoadInIe() {
if (this.isIe()) {
this._zone.run(() => setTimeout(() => this._appRef.tick(), 5));
}
}
}
Then in Every Route component that uses Lifecycle Hooks I called
constructor(private dialogService: ModalDialogService,ieHackService: IeHackService) {
ieHackService.onComponentLoadInIe();
}
I had this issue as well, I used a workaround to automatically refresh the page if the bug occurs, hoping that the bug will eventually be solved.
It's very ugly, but for now it works at least.
declare var Modernizr: any;
#Component({
selector : 'search-bar',
templateUrl: 'views/searchbar.html'
})
export class SearchBar {
private history: SearchHistoryEntry[] = [];
#Output() onHistory = new EventEmitter();
ie11hack: boolean = true;
constructor() {
this.history = JSON.parse(localStorage.getItem('SearchHistory')) || [];
if (!Modernizr.es6collections || navigator.userAgent.toLowerCase().indexOf("safari") !== -1) {
setTimeout(() => {
if (this.ie11hack) {
window.location.reload();
}
}, 500);
}
}
ngOnInit() {
ie11hack = false;
// The constructor doesn't have #Outputs initialized yet. Emit from this
// life cycle hook instead to be sure they're received on the other side
debugger;
this.onHistory.emit(this.history);
}
}
Edit, a less ugly partial fix:
The issues is (I think) caused by using a direct url rather then using the angular router (javascript).
If you want your website to work if people enter their url manually then you still need the above hack, but otherwise you may do what I did below (you may want to do that anyway).
I changed this:
<!-- html -->
<a href="#/objects/objectthingy/{{myObject.id}}" class="my-object-class">
To this:
<!-- html -->
<a href="javascript:void(0)" (click)="openMyObject(myObject.id)" class="my-object-class">
// typescript
openMyObject(objectId: number) {
this.router.navigate(['/Objects', 'ObjectThingy', { id: objectId}]);
}
and ngAfterViewInit method was called again.