How to get value from an array based on the user selection? - ember.js

I've follow the ember-paper guide and defined the options data as below. The user is able to select any country from the options.
timeZoneOptions: Object.freeze([
{ groupName: "Asia", options:["Kabul","Yerevan","Baku","Dhaka","Brunei","Bangkok","Shanghai","Urumqi","Taipei","Macau","Tbilisi","Dili","Kolkata","Jakarta"]},
{ groupName: "Australia", options: ["Darwin", "Eucla", "Perth", "Brisbane","Lindeman","Adelaide","Hobbart","Currie","Melbourne"]},
])
Here is the code for the select option. It will display the options groupby the groupName.
{{#paper-select options=this.timeZoneOptions
selected=this.timeZone
onChange=(action (mut this.timeZone)) as |timeZon| }}
{{timeZon}}
{{/paper-select}}
I can't get the data using {{this.timeZone.groupName}}.
How can I do if i want to get the groupName based on the option user selected?

What you have there seems correct. Maybe the error lies in the mut usage, maybe it's somewhere else.
The mut helper is quite vague. It is gonna be deprecated when the Ember team figures out how to do it gracefully.
You can avoid the mut helper by creating a distinct action on your controller/component.
This will let you debug: simply put a debugger statement into your action and proceed from there.
Classic Ember style:
import Component from '#ember/component';
export default Component.extend({
timeZoneOptions: Object.freeze([
{ groupName: "Asia", options:["Kabul","Yerevan","Baku","Dhaka","Brunei","Bangkok","Shanghai","Urumqi","Taipei","Macau","Tbilisi","Dili","Kolkata","Jakarta"]},
{ groupName: "Australia", options: ["Darwin", "Eucla", "Perth", "Brisbane","Lindeman","Adelaide","Hobbart","Currie","Melbourne"]},
]),
currentTimeZoneOption: null,
actions: {
selectTimeZoneOption(timeZoneOption) {
this.set('currentTimeZoneOption', timeZoneOption');
}
}
});
{{#paper-select
options=this.timeZoneOptions
selected=this.currentTimeZoneOption
onChange=(action 'selectTimeZoneOption')
as |timeZoneOption|
}}
{{timeZoneOption}}
{{/paper-select}}
<p>
Current timezone option:
{{this.currentTimeZoneOption.groupName}}
</p>
Ember Octane style:
import Component from '#glimmer/component';
import { tracked } from '#glimmer/tracking';
import { action } from '#ember/object';
export default class MyComponent extends Component {
timeZoneOptions = Object.freeze([
{ groupName: "Asia", options:["Kabul","Yerevan","Baku","Dhaka","Brunei","Bangkok","Shanghai","Urumqi","Taipei","Macau","Tbilisi","Dili","Kolkata","Jakarta"]},
{ groupName: "Australia", options: ["Darwin", "Eucla", "Perth", "Brisbane","Lindeman","Adelaide","Hobbart","Currie","Melbourne"]},
]);
#tracked
currentTimeZoneOption = null;
#action
selectTimeZoneOption(timeZoneOption) {
this.currentTimeZoneOption = timeZoneOption;
}
}
<div class="my-component">
<PaperSelect
#options={{this.timeZoneOptions}}
#selected={{this.currentTimeZoneOption}}
#onChange={{this.selectTimeZoneOption}}
as |timeZoneOption|
>
{{timeZoneOption}}
</PaperSelect>
<p>
Current timezone option:
{{this.currentTimeZoneOption.groupName}}
</p>
</div>

Related

Angular 2 ngFor object property change not updated its view

I have did the following
<div *ngFor="let item of documentData">
<polymer-component [data]="item"></polymer-component>
</div>
<button (click)="ChangePropertyValue()">ChangePropertyValue</button>
ChangePropertyValue(){
this.documentData[0].documentname="Document changed";
}
ngOnInit(){
this.documentData={"documentname":"Document"}
}
Polymer-Component has properties such as
documentname
When firing the ChangePropertyValue() , the object and its property is being updated but not its view. Please provide solution as soon as possible.
I have also tried ChangeDetectorRef,still it results nothing
Try running inside of angular's zone
import { NgZone } from '#angular/core';
constructor(private ngZone: NgZone) {
}
ChangePropertyValue(){
this.ngZone.run(() => {
this.documentData[0].documentname="Document changed"
})
}

Unit testing user interaction in an angular2 component

I've recently started learning angular2, and I thought I'd try and write a combobox component similar to select2. Here's a simplified version of the component:
#Component({
selector: 'combobox',
template: `
<div class="combobox-wrapper">
<input type="text"></input>
<button class="toggle-button"></button>
</div>
<div *ngIf="isDropdownOpen" class="dropdown">
<div *ngFor="let option of options; let index = index;"
class="option"
[class.focused]="focusedIndex==index">
{{option.label}}
</div>
</div>
`
})
export class ComboboxComponent {
#Input() options: any[] = [];
isDropdownOpen: boolean = false;
focusedIndex: number = 0;
}
I'm struggling when it comes to writing unit tests involving user interaction. For example, I want the user to be able to navigate the list of options by using the up and down keys on the keyboard. The way I see it, I have two options.
Option 1:
describe('when the dropdown is open and the user presses the "UP" key', () => {
it('should focus the first available preceeding option', () => {
const button = fixture.debugElement.query(By.css('.toggle-button'));
const input = fixture.debugElement.query(By.css('input'));
button.triggerEventHandler('mousedown', {});
input.triggerEventHandler('keydown', { key: 'ArrowDown' });
input.triggerEventHandler('keydown', { key: 'ArrowDown' });
input.triggerEventHandler('keydown', { key: 'ArrowUp' });
fixture.detectChanges();
const options = fixture.debugElement.queryAll(By.css('.option'));
expect(options[1].nativeElement.className).toContain('focused');
});
});
Option 2:
describe('when the dropdown is open and the user presses the "UP" key', () => {
it('should focus the first available preceeding option', () => {
const combobox = fixture.componentInstance;
combobox.isDropdownOpen = true;
combobox.focusedIndex = 2;
input.triggerEventHandler('keydown', { key: 'ArrowUp' });
fixture.detectChanges();
expect(combobox.focusedIndex).toBe(1);
});
});
Neither option feels right. In the first case I'm making assumptions about behaviour that is not part of the test itself - namely that clicking the "toggle" button will open the dropdown, and that pressing the "ArrowDown" key will focus the next option on the list.
In the second case I'm accessing properties that are not part of the component's public interface (#Inputs and #Outputs), and the test itself requires detailed knowledge about the actual implementation.
How should I approach this?

Angular 2 Unit Testing - Cannot read property 'root' of undefined

Error Description
Angular version: 2.3.1
My unit test fails to create the component - I know this issue is related to the [routerLink] and [routerLinkActive] directives because removing them from the template allows the test to create the component.
TEMPLATE
<nav class="navbar navbar-default navbar-fixed-top" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<button class="navbar-toggle" data-toggle="collapse" data-target="#iotahoe-top-navigation">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" [routerLink]="['/']">IoTahoe</a>
</div>
<div class="collapse navbar-collapse" id="iotahoe-top-navigation">
<ul *ngIf="isAuthenticated()" class="nav navbar-nav navbar-right">
<li [routerLinkActive]="['active']"><a [routerLink]="['/dashboard']">Dashboard</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/browse']">Browse</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/admin']">Admin</a></li>
<li [routerLinkActive]="['active']"><a (click)="onLogout()" style="cursor: pointer;">Logout</a></li>
</ul>
</div>
TYPESCRIPT
import { Component, OnInit } from '#angular/core';
import { AuthenticationService } from '../../authentication/authentication.service';
import { Router } from '#angular/router';
#Component({
moduleId: module.id.toString(),
selector: 'app-top-navbar',
templateUrl: './top-navbar.component.html',
styleUrls: ['./top-navbar.component.css']
})
export class TopNavbarComponent implements OnInit {
constructor(private authenticationService: AuthenticationService, private router: Router) { }
ngOnInit() {
}
isAuthenticated() {
return this.authenticationService.isLoggedIn;
}
onLogout() {
this.authenticationService.logout().subscribe(() => {
return this.router.navigate(['/login']);
});
}
}
TEST SPEC
/* tslint:disable:no-unused-variable */
import {async, ComponentFixture, TestBed, inject} from '#angular/core/testing';
import { By } from '#angular/platform-browser';
import {DebugElement, CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, Component} from '#angular/core';
import { RouterTestingModule } from '#angular/router/testing';
import { Location, CommonModule } from '#angular/common';
import { TopNavbarComponent } from './top-navbar.component';
import { AuthenticationService } from '../../authentication/authentication.service';
import { Router } from '#angular/router';
import {ReactiveFormsModule} from "#angular/forms";
#Component({
template: ''
})
class DummyComponent {
}
describe('TopNavbarComponent', () => {
let component: TopNavbarComponent;
let fixture: ComponentFixture<TopNavbarComponent>;
let authenticationService: AuthenticationService;
beforeEach(async(() => {
const authenticationServiceStub = {
isLoggedIn: false
};
const routerStub = {
navigate: jasmine.createSpy('navigate'),
navigateByUrl: jasmine.createSpy('navigateByUrl')
};
TestBed.configureTestingModule({
declarations: [ TopNavbarComponent, DummyComponent ],
imports:[CommonModule, ReactiveFormsModule, RouterTestingModule.withRoutes(
[
{ path: '/', component:DummyComponent },
{ path: '/login', component:DummyComponent },
{ path: '/dashboard', component:DummyComponent },
{ path: '/browse', component:DummyComponent },
{ path: '/admin', component:DummyComponent }
])],
providers: [
{ provide: AuthenticationService, useValue: authenticationServiceStub },
{ provide: Router, useValue: routerStub }
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TopNavbarComponent);
component = fixture.componentInstance;
authenticationService = TestBed.get(AuthenticationService);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
ERROR
zone.js:155 Uncaught Error: Error in package:407:9:6 caused by: Cannot
read property 'root' of undefined
at ViewWrappedError.Error (native)
at ViewWrappedError.ZoneAwareError (localhost:9876/base/src/test.ts:133296:33)
at ViewWrappedError.BaseError [as constructor] (localhost:9876/base/src/test.ts:35630:16)
at ViewWrappedError.WrappedError [as constructor] (localhost:9876/base/src/test.ts:35695:16)
at new ViewWrappedError (localhost:9876/base/src/test.ts:68018:16)
at DebugAppView._rethrowWithContext (localhost:9876/base/src/test.ts:108242:23)
at DebugAppView.create (localhost:9876/base/src/test.ts:108142:18)
at DebugAppView.View_TopNavbarComponent_Host0.createInternal (/DynamicTestModule/TopNavbarComponent/host.ngfactory.js:16:19)
at DebugAppView.AppView.createHostView (localhost:9876/base/src/test.ts:107700:21)
at DebugAppView.createHostView (localhost:9876/base/src/test.ts:108156:52)
at ComponentFactory.create (localhost:9876/base/src/test.ts:49830:25)
at initComponent (localhost:9876/base/src/test.ts:6425:53)
at ZoneDelegate.invoke (localhost:9876/base/src/test.ts:132727:26)
at ProxyZoneSpec.onInvoke (localhost:9876/base/src/test.ts:95802:39)
at ZoneDelegate.invoke (localhost:9876/base/src/test.ts:132726:32)Zone.runTask #
zone.js:155ZoneTask.invoke # zone.js:345data.args.(anonymous function)
# zone.js:1376
The routerLink directives need a real router, but you are mocking it. A couple things I can see you doing:
If you don't plan to click the links during testing, then you can mock those directive to just make "noop" directives so the compiler doesn't complain. e.g.
#Directive({
selector: '[routerLink], [routerLinkActive]'
})
class DummyRouterLinkDirective {}
Then just add that to the declarations of the test module. With this you don't really need to configure the RouterTestingModule at all. You could probably get rid of that.
Also if you don't plan to click test, another option (without needing to create dummy directives is to just ignore the errors of missing directives:
schemas: [ NO_ERRORS_SCHEMA ]
You would add this to the test module configuration (as seen here). This might not be desirable in some cases, as it could also ignore errors that you actually want detected, which could lead to hard to debug tests.
If you would actually like to click the links and test routing, then you can use the real router. You can see an example of how you can test the navigation, using the Location, as seen in this post.
For me the solution with the non-mocked routing worked. But I found out that I also needed to add a
<router-outlet></router-outlet>
to the component using "routerlink active".

How to access input fields of injected form component

I inject a component with an input form into a ionic2 form, both created with formBuilder.
From the address input component (search-map.ts):
#Component({
selector: 'search-map',
templateUrl: 'search-map.html'
})
#NgModule({
schemas: [ CUSTOM_ELEMENTS_SCHEMA ]
})
export class SearchMap {
public searchMapForm = this.formBuilder.group({
country: ["DE"],
postcode: ["", this.searchCont],
city: [""],
});
(...)
Inserted into the HTML of the base form (signup.html):
(...)
<ion-item>
<ion-label floating>Password</ion-label>
<ion-input type="password" formControlName="password"></ion-input>
</ion-item>
<search-map></search-map>
(...)
From the base form (signup.ts)
ionViewWillLoad() {
this.signup = this.formBuilder.group({
name: [this.name, Validators.required],
fullname: [this.fullname, Validators.compose([Validators.required, Validators.pattern('^[\\w-äöüßÄÖÜ]+(\\s[\\w-äöüßÄÖÜ]+)+$')])],
email: [this.email, Validators.compose([Validators.required, Validators.pattern('^(([^<>()\\[\\]\\\\.,;:\\s#"]+(\\.[^<>()\\[\\]\\\\.,;:\\s#"]+)*)|(".+"))#((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$')])],
password: ['', Validators.compose([Validators.required, Validators.minLength(8)])],
passwordRepeat: ['', this.passwordCompare],
address: [this.address, Validators.required],
});
}
Edit and change events are working fine on both.
How can I read the country and postcode input fields from signup.ts file (class signupPage)?
In your html where you set search-map,do this:
<search-map #searchMap></search-map>
In your SignupPage class,declare searchMap as
#ViewChild(SearchMap)
searchMap:SearchMap;
this way you can access the child component in the parent and all of the public properties of the child.(You may have to make searchMapForm a public property of searchmap class)
Check here for more

How to verify ACL in a template

How should a template lookup the user access to a specific route before displaying a link/action?
Considering the routes already contain a list of authorized roles, should a simple template helper/component lookup the view property and validates access?
( something like {{#if has-access-to 'items.new'}} ? )
Routes are currently "protected" using a simple ACL solution:
AclRouteMixin
import Ember from 'ember';
var accountTypes = {
1: 'member',
2: 'manager',
3: 'owner'
};
export default Ember.Mixin.create({
beforeModel: function beforeModel(transition) {
this._super(transition);
var accountType = this.get('session.accountType');
var role = accountTypes.hasOwnProperty(accountType) ? accountTypes[accountType] : 'unknown';
if (this.get('roles') && !this.get('roles').contains(role)) {
transition.abort();
this.transitionTo('unauthorized');
}
}
});
Route
export default Ember.Route.extend(AuthenticatedRouteMixin, AclRouteMixin, {
roles: [ 'manager', 'owner' ]
});
EDIT
Since the server knows the permissions it is much easier to include a policy object ( or per-entity properties ) than trying to duplicate the authorization logic.
This talk ( linked by MilkyWayJoe ) explains a really simple way to setup authentication / ACL.
The session object ( or each API response ) could contain a Policy object that contains true/false values.
Template
{{#if session.policy.canCreateItems}}{{link-to 'New Item' 'items.new'}}{{/if}}
{{#if item.policy.canEdit}}{{link-to 'Edit' 'items.edit' item}}{{/if}}
Authenticator ( if using ember-simple-auth )
var session = {
userId: response.user_id,
policy: {}
};
for(var p in response.policy) {
if (response.policy.hasOwnProperty(p)) {
session.policy[p.camelize()] = response.policy[p];
}
}
API responses
{
"items": [{
...
policy: {
can_delete: true,
can_view: true,
can_edit: true
}
}],
"policy": {
can_create: true
}
}
The way I would do it is load up the permissions on an auth route that all other routes extend, as for checking it and displaying links I went ahead with a component:
import Ember from 'ember';
var Component = Ember.Component;
export default Component.extend({
hasPermission: function() {
var permission = this.get('permission');
return this.get('auth.permissions').indexOf(permission) !== -1;
}.property('permission')
});
As for the template:
{{#if hasPermission}}
{{yield}}
{{/if}}
And simply call it from links:
{{#can-do permission="view_tables"}}
{{link-to "tables" "tables" class="nav__link"}}
{{/can-do}}
Hope it helps. Let me know if you have any questions.