The router that I have created successfully builds a navigation model, but it's missing to update some bindings that need to be updated each time a page is loaded within that childRouter (app.pageTitle, app.pageDescription).
Is there some way how to map these updates into Durandal's lifecycle, something like activate event?
define([ 'durandal/app', 'plugins/router', 'knockout', 'app' ], function(app, router, ko, app) {
console.log("content start");
var childRouter = router.createChildRouter().makeRelative({
moduleId : 'app/content/pages',
fromParent : true
}).map([ {
route : [ '', 'grid' ],
moduleId : 'grid/index'
}, {
route : 'details/:id',
moduleId : 'details/index'
}, {
route : 'details/tabs/base',
moduleId : 'details/tabs/base'
} ]).buildNavigationModel();
console.log("cms title start");
app.pageTitle(app.i18n('app:modules.content.title'));
app.pageDescription(app.i18n('app:modules.content.subtitle'));
console.log("cms title stop");
return {
router : childRouter
};
});
The simplest solution that works would be adding activate function and returning it. Durandal will then call it everytime a page from the childRouter navigation model is requested.
define([ 'durandal/app', 'plugins/router', 'knockout', 'app' ], function(app, router, ko, app) {
console.log("content start");
var childRouter = router.createChildRouter().makeRelative({
moduleId : 'app/content/pages',
fromParent : true
}).map([ {
route : [ '', 'grid' ],
moduleId : 'grid/index'
}, {
route : 'details/:id',
moduleId : 'details/index'
}, {
route : 'details/tabs/base',
moduleId : 'details/tabs/base'
} ]).buildNavigationModel();
function activate() {
console.log("cms title start");
app.pageTitle(app.i18n('app:modules.content.title'));
app.pageDescription(app.i18n('app:modules.content.subtitle'));
console.log("cms title stop");
}
return {
activate : activate,
router : childRouter
};
});
Related
I am new to writing unit test with Angular + Jasmine.
I have a custom directive and trying to write a unit test for it.
My directive has the following HostListener. I am not sure how to pass the 'event' object to this in spec.ts file
#HostListener('mouseup', ['$event'])
clickEvent(event: MouseEvent) {
if (this.hasIgnoreAttribute(event.target)) return; //event.target is undefined here.so i am not able to test the switch case. how to pass target and button properties from test?
switch (event.button) {
case 0: // Left click
this.navigate();
return;
case 1: {
// Middle click
this.openNewTab();
return;
}
}
}
spec.ts
beforeEach(() => {
spyRouter = jasmine.createSpyObj('Router', ['navigate']);
TestBed.configureTestingModule({
declarations: [RowClickTestComponent, RowClickDirective],
providers: [
{ provide: ContextMenuService, useValue: spyContextMenuService },
{ provide: Router, useValue: spyRouter }
]
});
fixture = TestBed.createComponent(RowClickTestComponent);
component = fixture.componentInstance;
inputEl = fixture.debugElement.query(By.css('tr'));
});
fit('mouseup on row', () => {
inputEl.triggerEventHandler('mouseup', { } as MouseEvent); //how to pass the 'target'
fixture.detectChanges();
});
I'm following the documentation for mobx-react-router but upon attempting to run my application I get the following error in the browser:
Uncaught TypeError: An element descriptor's .kind property must be either "method" or "field", but a decorator created an element descriptor with .kind "undefined"
at _toElementDescriptor (app.js:49988)
at _toElementFinisherExtras (app.js:49990)
at _decorateElement (app.js:49980)
at app.js:49976
at Array.forEach (<anonymous>)
at _decorateClass (app.js:49976)
at _decorate (app.js:49958)
at Module../src/App/UserStore.js (app.js:50012)
at __webpack_require__ (bootstrap:19)
at Module../src/index.js (index.js:1)
Here is how I intitialize:
const appContainer = document.getElementById('app');
if(appContainer) {
const browserHistory = createBrowserHistory()
const routingStore = new RouterStore();
const stores = {
users: userStore,
routing: routingStore
}
const history = syncHistoryWithStore(browserHistory, routingStore);
ReactDOM.render(
(
<Provider {...stores}>
<Router history={history}>
< App />
</Router>
</Provider>
),
appContainer);
}
And this is how I use:
#inject('routing')
#inject('users')
#observer
class App extends Component { ...
My UserStore:
import { observable, action, computed } from "mobx"
class UserStore {
#observable users = [];
#action addUser = (user) => {
this.users.push(user)
}
#computed get userCount () {
return this.users.length
}
}
const store = new UserStore();
export default store;
I've tried to Google for this error but it's returning no useful results. Any ideas what I am doing wrong?
If you're using Babel 7 install support for decorators:
npm i -D\
#babel/plugin-proposal-class-properties\
#babel/plugin-proposal-decorators
Then enable it in your .babelrc or webpack.config.js file:
{
"plugins": [
["#babel/plugin-proposal-decorators", { "legacy": true}],
["#babel/plugin-proposal-class-properties", { "loose": true}]
]
}
Note that the legacy mode is important (as is putting the decorators proposal first). Non-legacy mode is WIP.
Reference: https://mobx.js.org/best/decorators.html
While the accepted answer works, for anyone coming here from the same error message but a different context, this is likely because the decorator's signature changed as the proposal progressed.
Using { "legacy": true } uses one method signature, while {"decoratorsBeforeExport": true } uses another. The two flags are incompatible.
You can inspect what's happening with the given example
function log() {
console.log(arguments)
}
class Foo
#log
bar() {
console.log('hello')
}
}
new Foo().bar()
Using {"decoratorsBeforeExport": true } will yield
[Arguments] {
'0': Object [Descriptor] {
kind: 'method',
key: 'bar',
placement: 'prototype',
descriptor: {
value: [Function: bar],
writable: true,
configurable: true,
enumerable: false
}
}
}
Where {"legacy": true } would give you
[Arguments] {
'0': Foo {},
'1': 'bar',
'2': {
value: [Function: bar],
writable: true,
enumerable: false,
configurable: true
}
}
By not using the legacy semantics, writing a decorator is fairly simple. A decorator which returns its 0th argument is a no-op. You can also mutate before returning. Using the new semantics, you can describe a decorator as follows:
function log(obj) {
console.log('I am called once, when the decorator is set')
let fn = obj.descriptor.value
obj.descriptor.value = function() {
console.log('before invoke')
fn()
console.log('after invoke')
}
return obj
}
I changed the observable like so and it works (although not sure why):
import { observable, action, computed } from "mobx"
class UserStore {
#observable users;
constructor() {
this.users = []
}
#action addUser = (user) => {
this.users.push(user)
}
}
const store = new UserStore();
export default store;
I want to load menu options dynamically. so I'm wondering the best approach
I am able to use the code below to add routes after the page is loaded. This works for normal navigation, but does not work during a refresh.
Can configure router return a promise / how do I load menu items into the route?
#inject(HttpClient)
export class DocumentMenu {
router: Router;
documents : IDocument[];
heading = 'Document Router';
constructor(public http: HttpClient) {}
activate(): void {
this.http.fetch('http://localhost:17853/Document/GetDocuments?folderID=13244')
.then<IDocument[]>(response => response.json())
.then<IDocument[]>(docs => {
if ( docs ){
for( var doc of docs){
this.router.addRoute( { route : doc.DocumentID.toString(), name : doc.Name, moduleId: './documents/document', nav:true, title: doc.Name });
}
this.router.refreshNavigation();
}
return docs;
});
}
configureRouter(config: RouterConfiguration, router: Router) {
var routes = new Array();
routes.push(
{ route: 'index', name: 'index-name', moduleId: './documents/index', nav: false, title: 'Documents' } );
routes.push( { route: '', redirect: 'index' } );
config.map( routes );
this.router = router;
}
}
This does not answer your question, but I think it may be helpful to you and others with a similar issue.
The Dynamic Route Anti-Pattern
Your application has a number of different routes, all of which vary based on the state of the application. Therefore, you must first fetch the data, and then build the routes, and then register them with the router.
The reason this is an anti-pattern is because you will continuously need to update the router based on the state of the application, when Aurelia itself is built with static ways of describing dynamic content.
Dynamically Routing Homogeneous Data
Let's say you are building Google Drive, and you have a number of various files that could change as the user adds and removes them. For this case you have two categories of routes: Folders and Documents. Therefore, you make one route for each.
configureRouter(config) {
config.map([
{ route: 'folder/:id', moduleId: 'folder' }
{ route: 'document/:id', moduleId: 'document' }
}
}
class FolderViewModel {
activate({ id }) {
// get your specific folder data and load it into your folder view model
this.fetch('getDocuments?folderId=${id}')
}
}
class DocumentViewModel {
activate({ id }) {
// get your specific document and load it into your document view model
this.fetch('getDocuments?documentId=${id}')
}
}
Dynamically Routing Hetergeneous Data
Let's say instead you want to build YouTube. When user mjd10d logs in, he is welcome to watch videos to his heart's content, but he is not a premium content creator, and doesn't have access to the content creation portion of the site. The best way to handle this is to leave all possible routes in your application, and filter them based on the user's credentials in an AuthorizeStep.
configureRouter(config, router) {
config.addPipelineStep('authorize', AuthorizeStep);
}
#inject(UserSession)
class AuthorizeStep {
constructor(UserSession) {
this.user = UserSession;
}
run(navigationInstruction, next) {
var instructions = navigationInstruction.getAllInstructions()
if (!this.authorized(instructions.config)) {
return Redirect('404');
}
return next();
}
authorized(routeConfig) {
// something smart that returns false if unauthorized
return this.user.permissionLevel > routeConfig.requiredPermission;
}
}
Though not all cases will be authorization related, you can always register your own pipeline step using the addPipelineStep API
You can add routes dynamically (at startup or anytime for that matter) by having a single fixed (static) route in the "configureRouter" method (in app.ts), to which you then add all the other routes dynamically, when your fetch completes, like so:
configureRouter(config, router) {
config.title = 'SM';
//configure one static route:
config.map([
{ route: ['', 'welcome'], name: 'welcome', moduleId: 'welcome/welcome', title: 'Welcome' }
]);
routeMaps(this.navRepo) //your repo/service doing the async HTTP fetch, returning a Promise<Array<any>> (i.e., the routes)
.then(r => {
r.forEach(route => this.router.addRoute(route));
//once all dynamic routes are added, refresh navigation:
this.router.refreshNavigation();
});
this.router = router;
}
The "routeMaps" function is just a wrapper around the repo call and a mapping of the result to the Array of route items.
You can return a promise in activate. if activate() returns a promise, configureRouter() doesnt fire until the promise returned in activate() is resolved.
I ended up preparing the routes in activate like below:
activate(){
return this.http.fetch('url')
.then(response => response.json())
.then(docs => {
this.routerMapped = docs;
});
}
configureRouter(config, router) {
//build the routes from this.routermapped if necessary
config.map( this.routerMapped );
this.router = router;
}
To make this work, I created the routes in the constructor with a synchronous request
export class DocumentMenu {
...
routes : RouteConfig[];
constructor(http: HttpClient) {
this.http = http;
var folderID = window.location.hash.split('/')[2]
this.routes = new Array<RouteConfig>();
this.routes.push ( { route: 'index', name: 'index-name', moduleId: './documents/index', nav: false, title: 'Documents' });
this.routes.push ( { route: '', redirect: 'index' } );
for( var route of this.getRoutes( folderID )){
this.routes.push( route );
}
}
getRoutes(folderID: string) : RouteConfig[]
{
var routes = new Array<RouteConfig>();
var docsURL = 'http://localhost:17853/Document/GetDocuments?folderID=' + folderID;
// synchronous request
var docsResp = $.ajax({
type: "GET",
url: docsURL,
async: false,
cache:false
}).responseText;
var docs = JSON.parse( docsResp );
for( var doc of docs ){
routes.push( { route : doc.DocumentID.toString(), name : doc.Name, moduleId: './documents/document', nav:true, title: doc.Name });
}
return routes;
}
configureRouter(config: RouterConfiguration, router: Router) {
config.map( this.routes );
this.router = router;
}
...
I'm trying to use backbonejs as my router, routing does not seems to work for subpath nor splat. I'm using MVC 4 together with Jquery mobile for this project and hopes it does not cause conflict in the routing.
The result of testing are below:
http://localhost gives me "Routed to home" in console. (correct)
http://localhost/#contacts gives me "Routed to contacts list" in console. (correct)
http://localhost/#contacts?1 stills gives me "Routed to contacts list" in console. (wrong)
http://localhost/#contacts/view/1 redirects me to http://localhost/contacts/view/1 and gives me a 404 error since I do not have such page. (wrong)
I have tried using splats, and I got the exact same problem as my 4th example. Please guide me on what I might be doing wrong.
Here's my code sample.
app.js
define([
'jquery',
'backbone',
'router',
], function ($, Backbone, Router) {
var initialize = function () {
$(document).on("mobileinit",
// Set up the "mobileinit" handler before requiring jQuery Mobile's module
function () {
$.mobile.ajaxEnabled = false
$.mobile.hashListeningEnabled = false
$.mobile.linkBindingEnabled = false
$.mobile.pushStateEnabled = false
$('div[data-role="page"]').live('pagehide', function (event, ui) {
$(event.currentTarget).remove();
})
});
require(["jquerymobile"], function () {
// Instantiates a new Backbone.js Mobile Router
this.router = new Router();
});
};
return {
initialize: initialize
};
});
router.js
define([
'jquery',
'jquerymobile',
'underscore',
'backbone',
'../../scripts/backbone/views/home/HomeView',
'../../scripts/backbone/views/footer/FooterView',
], function ($, Mobile, _, Backbone, HomeView, FooterView) {
var AppRouter = Backbone.Router.extend({
initialize: function () {
var homeView = new HomeView();
homeView.render();
var footerView = new FooterView();
footerView.render();
Backbone.history.start({ pushState: false });
},
routes: {
'contacts': 'contactList',
'contacts?:id': 'contactsDetail',
'contacts/view/:id': 'contactsDetail',
//'*actions': 'home',
'': 'home'
},
// Home method
home: function () {
console.log("Routed to home");
},
contactList: function () {
console.log("Routed to contacts list");
},
contactsDetail: function (id) {
console.log("Routed to contacts detail");
}
});
return AppRouter;
});
I am developing a Sencha Touch 2 app with user authentication.
I use a token for authentication.
The logic.
Check is a token exists in local storage:
var tokenStore = Ext.getStore('TokenStore'),
token = tokenStore.getAt(0).get('token');
If there is a token, check if it's valid.
I am doing a read from a model which is connected to my API which, returns success or fail - depending on the token - if it's valid or not.
TestApp.model.CheckAuthModel.load(1, {
scope: this,
success: function(record) {
// Here, I know the token is valid
},
failure: function() {
console.log('failure');
},
callback: function(record) {
console.log('callback');
console.log();
}
});
And here is the router, which handles the logic for the views:
Ext.define("TestApp.controller.Router", {
extend: "Ext.app.Controller",
config: {
refs: {
HomeView: 'HomeView',
LoginView: 'LoginView',
ProductsView: 'ProductsView',
ProductsViewTwo: 'ProductsViewTwo'
},
routes: {
'': 'home',
'home' : 'home',
'login' : 'login',
'products' : 'products',
'testingtwo' : 'testingtwo'
}
},
home: function () {
console.log('TestApp.controller.Router home function');
var initialItem = Ext.Viewport.getActiveItem(),
comp = this.getHomeView();
if (comp === undefined) comp = Ext.create('TestApp.view.HomeView');
Ext.Viewport.animateActiveItem(comp, {
type: 'slide',
listeners: {
animationend: function() {
initialItem.destroy();
}
}
});
},
login: function () {
var initialItem = Ext.Viewport.getActiveItem(),
comp = this.getLoginView();
if (comp === undefined) comp = Ext.create('TestApp.view.LoginView');
Ext.Viewport.animateActiveItem(comp, {
type: 'slide',
listeners: {
animationend: function() {
initialItem.destroy();
}
}
});
},
products: function () {
var initialItem = Ext.Viewport.getActiveItem(),
comp = this.getProductsView();
if (comp === undefined) comp = Ext.create('TestApp.view.ProductsView');
Ext.Viewport.animateActiveItem(comp, {
type: 'slide',
listeners: {
animationend: function(){
initialItem.destroy();
}
}
});
},
testingtwo: function () {
var initialItem = Ext.Viewport.getActiveItem(),
comp = this.getProductsViewTwo();
if (comp === undefined) comp = Ext.create('TestApp.view.ProductsViewTwo');
Ext.Viewport.animateActiveItem(comp, {
type: 'slide',
listeners: {
animationend: function(){
initialItem.destroy();
}
}
});
},
launch: function() {
console.log('TestApp.controller.Router launch!');
}
});
Now, how can I link the router with the check auth model callback?
I want to know the auth state when the app reaches the router.
In other MVC frameworks, I could do a before filter, on the router, check for auth and handle the routes accordingly.
Can i do this in Sencha Touch 2?
Any ideas?
Hi I think this section in the documentation is exactly what you need:
before : Object
Provides a mapping of Controller functions to filter functions that are run before them when dispatched to from a route. These are usually used to run pre-processing functions like authentication before a certain function is executed. They are only called when dispatching from a route. Example usage:
Ext.define('MyApp.controller.Products', {
config: {
before: {
editProduct: 'authenticate'
},
routes: {
'product/edit/:id': 'editProduct'
}
},
//this is not directly because our before filter is called first
editProduct: function() {
//... performs the product editing logic
},
//this is run before editProduct
authenticate: function(action) {
MyApp.authenticate({
success: function() {
action.resume();
},
failure: function() {
Ext.Msg.alert('Not Logged In', "You can't do that, you're not logged in");
}
});
}
});
http://docs.sencha.com/touch/2.3.1/#!/api/Ext.app.Controller-cfg-before
Of course, it's still up to you to decide whether you should check every time or should cache the auth result for sometime.
Updated to answer comment below
Honestly, i am not sure how they was going to declare that static method Authenticate in Sencha (you would be able to do it normally through Javascript i think, i.e.: prototype).
But there are other better options to solve just that Authenticate function:
Just create a singleton class that handle utility stuffs.
http://docs.sencha.com/touch/2.3.1/#!/api/Ext.Class-cfg-singleton
If you really want to use MyApp, you can declare within the Ext.app.Application (in app.js). Then call it from the global instance MyApp.app.some_function(). I wouldn't exactly recommend this method because you change app.js, that might bring problem if you upgrade sencha touch.
You could implemented auth check in application's launch function or in your auth controller's init function and based on the response redirect the to appropriate url. Something like this:
TestApp.model.CheckAuthModel.load(1, {
scope: this,
success: function(record) {
this.redirectTo("home/");
},
failure: function() {
this.redirectTo("login/");
console.log('failure');
},
callback: function(record) {
console.log('callback');
console.log();
}
});