How to realize website with hundreds of pages in Angular2 - templates

I am preparing SPA website containing hundreds of article-like pages (apart from eCommerce, login etc.). Every article has its own URL. I want to realize it using Angular2.
The only solution I found so far is:
1. to prepare hundreds of Agular2 components, one component for every article...
...with templateUrl pointing to article markup. So I will need hundreds of components similar to:
#core.Component({
selector: 'article-1',
templateUrl: 'article1.html'
})
export class Article1 {}
2. to display an article using AsyncRoute
see Lazy Loading of Route Components in Angular2
#core.Component({
selector: 'article-wrapper',
template: '<router-outlet></router-outlet>'
})
#router.RouteConfig([
new router.AsyncRoute({
path: '/article/:id',
loader: () => {
switch (id) {
case 1: return Article1;
case 2: return Article2;
//... repeat it hundreds of times
}
},
name: 'article'
})
])
class ArticleWrapper { }
In Angular1 there was ngInclude directive, which is missing in Angular2 due to the security issues (see here).
[Edit 1] There is not only problem with the code itself. Problem is also with static nature of this solution. If I need website with sitemap and dynamic page structure - adding a single page needs recompilation of the whole ES6 JavaScript module.
[Edit 2] The concept "markup x html as data" (where markup is not only static HTML but also HTML with active components) is basic concept of whole web (every CMS has its markup data in database). If there does not exist Angular2 solution for it, it denies this basic concept. I believe that there must exist some trick.

All following solutions are tricky. Official Angular team support issue is here.
Thanks to #EricMartinez for pointing me to #alexpods solution:
this.laoder.loadIntoLocation(
toComponent(template, directives),
this.elementRef,
'container'
);
function toComponent(template, directives = []) {
#Component({ selector: 'fake-component' })
#View({ template, directives })
class FakeComponent {}
return FakeComponent;
}
And another similar (from #jpleclerc):
#RouteConfig([
new AsyncRoute({
path: '/article/:id',
component: ArticleComponent,
name: 'article'
})
])
...
#Component({ selector: 'base-article', template: '<div id="here"></div>', ... })
class ArticleComponent {
public constructor(private params: RouteParams, private loader: DynamicComponentLoader, private injector: Injector){
}
ngOnInit() {
var id = this.params.get('id');
#Component({ selector: 'article-' + id, templateUrl: 'article-' + id + '.html' })
class ArticleFakeComponent{}
this.loader.loadAsRoot(
ArticleFakeComponent,
'#here'
injector
);
}
}
A bit different (from #peter-svintsitskyi):
// Faking class declaration by creating new instance each time I need.
var component = new (<Type>Function)();
var annotations = [
new Component({
selector: "foo"
}),
new View({
template: text,
directives: [WordDirective]
})
];
// I know this will not work everywhere
Reflect.defineMetadata("annotations", annotations, component);
// compile the component
this.compiler.compileInHost(<Type>component).then((protoViewRef: ProtoViewRef) => {
this.viewContainer.createHostView(protoViewRef);
});

Related

How to test vue Js metaInfo

Am writing some unit testing, I have a component with meta info set using Vue-meta
My Component looks like this.
export default {
...
metaInfo () {
const expertName = this.getBlogInfo.blog.author.trim()
const fullName = expertName ? `${expertName.first_name} ${expertName.last_name}` : 'Cowsoko'
return {
title: `Dairynomics - Blog post from ${fullName}`,
meta: [
{
vmid: 'og:description',
name: 'og:description',
content: this.description
},
{
vmid: 'og:image',
name: 'og:image',
content: this.getBlogInfo.blog.photo
}
]
}
}
...
There's an issue on their github repo which says you need to create a local Vue instance.
You can read about local Vue instances in the vue-test-utils docs. It allows you to add components, mixins and install plugins without polluting the global Vue class, i.e. add in the vue-meta properties for this test only.
import { shallowMount, createLocalVue } from '#vue/test-utils'
import Component from './Component.vue'
import VueMeta from 'vue-meta'
let localVue = createLocalVue();
localVue.use(VueMeta);
describe('Component.vue', function() {
// Set up the wrapper
const wrapper = shallowMount(Component)
it('has a getTitle() method that returns the page title', () => {
expect(wrapper.vm.getTitle()).toBe(title)
})
it('has its meta title correctly set', () => {
expect(wrapper.vm.$meta().refresh().metaInfo.title).toBe('some title')
})
})
You can insert your meta data normally in each component.
If your pages are dynamic and if you want any dynamic SEO or meta tags you can use vue-headful.
Like this
<vue-headful
title="Title from vue-headful"
description="Description from vue-headful"
/>
In vue-headful you can write all the meta tags.

Static type for ionic 2 Page

I am trying to add a static type to my .ts file which is based off of a conditional in a click event, which equals an imported page.
gotoNext(pageData) {
this.conditionalPage = pageData.hasList ? ListPage : NoListPage;
this.navCtrl.push(this.conditionalPage, pageData);
}
At the top of the .ts file i have
import { ListPage, NoListPage } from '../../collections/app.pages';
#Component({
selector: 'page-program',
templateUrl: 'program.html'
})
export class ProgramPage {
conditionalPage: any; // I would prefer this not to be any
...
I want to change the any to represent the ionic 2 Page, type just not too sure what type it is.
I think ionic2 pages are angular2 component so you can use:
conditionalPage: Component;
but the ionic2 tutorial use any as a type:
conditionalPage: any;
The type you are looking for is Component.
Try:
conditionalPage:Component;

Ionic 2: How to use custom build Cordova Plugin

I'm already created cordova plugin and already used in Ionic 1, its worked. Then I tried to use it in Ionic 2 but I don't really know how to call that plugin. I follow the step from here to create my own plugin. And this is what i did:
plugin.xml
<name>myPlugin</name>
<js-module src="www/myPlugin.js" name="myPlugin">
<clobbers target="myPlugin" />
</js-module>
myPlugin.js
module.exports = {
myFunction: function (success, failure) {
cordova.exec(success, failure, "myPlugin", "myFunction", []);
}
};
hello-ionic.ts
import { Component } from '#angular/core';
declare var cordova: any;
#Component({
selector: 'page-hello-ionic',
templateUrl: 'hello-ionic.html'
})
export class HelloIonicPage {
constructor() {
}
click() {
if (typeof cordova !== 'undefined') {
cordova.plugins.myPlugin.myFunction();
}
}
}
But unfortunately it return me an error "Undefined myFunction" in hello-ionic.ts.
Here is what I did.
hello-ionic.ts
import { Component } from '#angular/core';
declare var myPlugin: any;
#Component({
selector: 'page-hello-ionic',
templateUrl: 'hello-ionic.html'
})
export class HelloIonicPage {
constructor() {
}
click() {
myPlugin.myFuntion(
(data) => {
console.log(data);
},
(err) => {
console.log(err);
});
}
}
declare var myPlugin: any; , myPlugin name I get from <clobbers target="myPlugin" />.
Note: Need to run the project in device only.
Following tutorial is a good resource to learn how to create custom cordova plugin :
https://taco.visualstudio.com/en-us/docs/createplugintutorial/
I have followed this tutorial to create multiple custom plugins and those are working fine in Ionic2.
One more thing to point out that the tutorial has not mentioned that:
You have to add your custom plugin in your ionic 2 project using following command:
ionic plugin add "folder path of your custom plugin"
Updated:
In your plugin.xml file, you have set "myPlugin" as target in clobbers tag.
So you should call your function as followed
window.myPlugin.myFunction();
Tip: Whenever you use custom plugin created by you(or someone else), inspect the application using Chrome Developer tools. In console tab of developer tools, you can inspect the window and other available objects and can find out correct way to call plugin's methods.

ionicView.enter not fired on child view and its controller

I have two nested states, consisted of a parent abstract state and a child state:
.state('app.heatingControllerDetails', {
url: "/clients/:clientId/heatingControllers/:heatingControllerId",
abstract: true,
views: {
'menuContent': {
templateUrl: "templates/heatingController.html",
controller: 'HCDetailsCtrl'
}
}
})
.state('app.heatingControllerDetails.wdc', {
url: "/wdc",
views: {
'hc-details': {
templateUrl: "templates/heatingControllers/wdc.html",
controller: 'WdcDetailsCtrl'
}
},
resolve:{
hcFamily: [function(){
return 'wdc';
}]
}
})
and two controllers are:
.controller('HCDetailsCtrl',function($scope){
$scope.$on("$ionicView.enter", function (scopes, states) {
...
});
})
.controller('WdcDetailsCtrl',function($scope){
$scope.$on("$ionicView.enter", function (scopes, states) {
...
});
})
When I invoke state app.heatingControllerDetails.wdc, both controllers are created, but $ionicView.enter is only invoked on the parent controller. Any idea?
In heatingController.html, hc-details view is defined as follows:
<ion-content class="has-header" ng-show="hc">
<div ui-view name="hc-details"></div>
<div class="disableContentDiv" ng-hide="hc.state=='Online'"></div>
</ion-content>
When working with nested views, you have to use $ionicNavView events instead of $ionicView
That being said, at the last release these events are bugged, and they're currently working in a fix: Github issue
I found a work around for this. Place this in a parent of the view, or place it in the first controller that is loaded when running your application.
You can use this to observe any 'to and from' view changes. Just doing a string comparison on the url of the toState emulated ionicView.enter well enough to achieve what I needed it for. Keep in mind you need to be using UI-router to do this. Hope this helps!
$rootScope.$on('$stateChangeStart',
function(event, toState, toParams, fromState, fromParams, options){
if(toState.url == "/video/:Id"){
console.log("Leaving the view.");
}
});

Angular JS/Karma Unit Testing with Angular UI-Router

I'm working on an Angular app that uses Angular UI-Router for routing and maintaining state. I am having a hard time getting my unit test to even compile. It seems that no matter what I do, I keep getting the following error:
Error: State '' is not navigable
I have the following states set up in my app, as well as the following redirects:
app.config(function($stateProvider, $urlRouterProvider) {
var paramList = '?beds&lat&lon&zoom';
$urlRouterProvider
.when('', '/')
.when('/', '/search')
.when('/search', '/search/list')
.otherwise('');
$stateProvider
.state('search', {
abstract: true,
url: '/search',
templateUrl: 'views/search.html',
controller: 'SearchCtrl'
})
.state('search.list', {
url: '/list' + paramList,
templateUrl: 'views/search-list.html'
})
.state('search.map', {
url: '/map' + paramList,
templateUrl: 'views/search-map.html'
})
.state('search.listing', {
url: '/listing/:id' + paramList,
templateUrl: 'views/search-listing.html'
})
.state('favorites', {
url: '/favorites',
templateUrl: 'views/favorites.html'
})
});
I've eaten up an entire day trying to get a single test to work, but no matter what approach I take, I always end up at this same error. I have an e2e test running just fine.
ui-router adds an internal state for '' automatically, so that is the error you are getting. I recommend adding a '/' url for a base state and change the 'otherwise' invocation to redirect to '/' instead of ''. The internal '' state is being attempted because of your .otherwise('') call. So perhaps try this:
$urlRouterProvider
.when('/', '/search')
.when('/search', '/search/list')
.otherwise('/');
At first glance I would say it looks like the $stateProvider is telling you you need to put an empty state in there. Add this to the list of states.
state( '', { ... } )