Angular 1.5 component router sibling components - javascript

Is there a way with the new component router for Angular 1.5 to keep the sibling component rendered in the ng-outlet directive?
I want to show the Detail View in parallel with the sibling List View.
As far as I understand the official Docs (https://docs.angularjs.org/guide/component-router) it should be possible with the $$router and bind it to the child component.
Here is what I tried to do:
http://plnkr.co/edit/KzW8fLAxrte9jSg5jhEg?p=preview
<ng-outlet><crisis-detail $router="$$router"></crisis-detail>
There i a similiar post on this binding topic:
Angular 1.5 component $router binding

There is no ability to show siblings simultaneously with Angular 1.5 Component Router as far as I know.
However, workaround is to make sibling to be actually child, and then use empty component to show with default, "no details" route.
Workaround:
First, we need some root component to activate list itself:
.component('listRoot', {
template: '<ng-outlet></ng-outlet>', //just ng-outlet, to render List inside
$routeConfig: [
{path: '/...', name: 'ListRoot',component: 'list' },
]
})
Then we need to add components for list, detail, and noDetail mode.
.component('list', {
template: 'List ... <ng-outlet></ng-outlet>',
$routeConfig: [
{path: '/', name: 'List',component: 'noDetails', useAsDefault: true },
{path: '/:id',name: 'Details',component: 'details'}
],
bindings: {
$router: '<'
},
controller: function () {
var ctrl = this
$routerOnActivate = function(route) {
ctrl.router = this.$router;
}
this.goToDetails = function(id) {
ctrl.$router.navigate(['Details', {id: id}])
}
}
})
.component('detail', {
template: 'Details: <a ng-link="[\'List\']">Go Back</a>'
})
.component('noDetails', {
template: '' //just empty template
})
Accessing parent:
Also, to be able to notify parent (in your example - sibling Detail component telling it ID to List, and List marking it as selected after) you can use require component option, to be able to access parent component scope.
.component('detail', {
template: 'Details: <a ng-link="[\'List\']">Go Back</a>',
require: {
parent: '^list'
},
controller: {
this.goBackWithNotify = function(data) {
ctrl.parent.someParentComponentProperty = data;
}
}
})
Edited plunker with example.
PS: I used more recent version of router.

Related

$onChanges not triggered

Get updated by model but don't update the model. Should I use the $onChanges function
I have a model:
class Model {
constructor(data) {
this.data = data;
}
getData() {
return this;
}
}
2 nested components:
var parentComponent = {
bindings: {
vm: '<'
},
controller: function() {
var ctrl = this;
},
template: `
<div>
<a ui-sref="hello.about" ui-sref-active="active">sub-view</a>
Parent component<input ng-model="$ctrl.vm.data">
<ui-view></ui-view>
</div>
`
};
var childComponent = {
bindings: {
vm: '<'
},
template: `
<div>
Child component <input ng-model="$ctrl.vm.data">
<br/>
Child component copy<input ng-model="$ctrl.vmCopy.data">
<br/>
Child component doCheck<input ng-model="$ctrl.vmCheck.data">
</div>
`,
controller: function() {
var ctrl = this;
ctrl.$onChanges = function(changes) {
ctrl.vmCopy = angular.copy(ctrl.vm);
ctrl.vm = ctrl.vm;
};
ctrl.$doCheck = function () {
var oldVm;
if (!angular.equals(oldVm, ctrl.vm)) {
oldVm = angular.copy(ctrl.vm);
ctrl.vmCheck = oldVm;
console.log(ctrl)
}
}
}
}
Both get data from resolve:
.config(function($stateProvider) {
var helloState = {
name: 'hello',
url: '/hello',
resolve: {
vm: [function() {
return myModel.getData();
}]
},
component: 'parent'
}
var aboutState = {
name: 'hello.about',
url: '/about',
resolve: {
vm: [function() {
return myModel.getData();
}]
},
component: 'child'
}
$stateProvider.state(helloState);
$stateProvider.state(aboutState);
})
I would like my components to be updated when model change or when parent change, but I don't wan't them to update the model.
I thought that was why one way binding '<' stands for.
Here's a fiddle to illustrate what I want to do.
In other word:
I would like the child component to be updated on parent changes but don't want the child to update the parent.
You can see in the fiddle that if I bind directly to local scope, child get update from parent but also update the parent
If I copy the binding to local scope, child isn't updating the parent but also doesn't get updated by parent.
If
With object content — Use the $doCheck Life-cycle Hook1
When binding an object or array reference, the $onChanges hook only executes when the value of the reference changes. To check for changes to the contents of the object or array, use the $doCheck life-cycle hook:
app.component('nvPersonalTodo', {
bindings: {
todos: "<"
},
controller: function(){
var vm = this;
this.$doCheck = function () {
var oldTodos;
if (!angular.equals(oldTodos, vm.todos)) {
oldTodos = angular.copy(vm.todos);
console.log("new content");
//more code here
};
}
})
From the Docs:
The controller can provide the following methods that act as life-cycle hooks:
$doCheck() - Called on each turn of the digest cycle. Provides an opportunity to detect and act on changes. Any actions that you wish to take in response to the changes that you detect must be invoked from this hook; implementing this has no effect on when $onChanges is called. For example, this hook could be useful if you wish to perform a deep equality check, or to check a Date object, changes to which would not be detected by Angular's change detector and thus not trigger $onChanges. This hook is invoked with no arguments; if detecting changes, you must store the previous value(s) for comparison to the current values.
— AngularJS Comprehensive Directive API Reference -- Life-cycle hooks
For more information,
AngularJS angular.equals API Reference
AngularJS 1.5+ Components do not support Watchers, what is the work around?

Binding '&' method in a routed component

Consider this simplified Angular 1.5.x component (all in jsfiddle):
appModule.component('mainComponent', {
controller: function() {
var x = 0;
this.broadcast = function() {
this.onUpdate({
count: x++
});
};
this.broadcast();
},
bindings: {
onUpdate: '&'
},
template: '<input type="button" ng-click="$ctrl.broadcast()" value="add"/>'
});
Html (in body)
<main-component on-update="this.count = count"></main-component>
Value in parent: {{count}}
When clicking the component button, the count variable is being update ('&' onUpdate is binded well).
Now I would like to have a route to the component from ui.router:
$stateProvider.state({
name: 'state1',
component: 'mainComponent',
url: "#"
});
Navigating to the state, results in Cannot read property '2' of null, removing the onUpdate member fix the error but break the binding.
What am I doing wrong? What is the way to bind callback methods of components when using ui.router route to components.
jsfiddle
it looks like binding callbacks using "resolve" is not currently supported, see here: https://github.com/angular-ui/ui-router/issues/2793

Configure ui-router with components containing multiple bindings

I am trying to find a better solution to use the ui-router together with angular components.
Consider two simple components:
app.component('componentOne', {
template: '<h1>My name is {{$ctrl.name}}, I am {{$ctrl.age}} years old.</h1>',
bindings : {
name : '#',
age : '#'
}
}
);
app.component('componentTwo', {
template: '<h1>I am component 2</h1>'
});
Right now, I am specifying the component and its parameter using the template property:
app.config(function($stateProvider, $urlRouterProvider ){
$stateProvider
.state('component1', {
url: "/component1",
template: "<component-one name=\"foo\" age=\"40\"></component-one>"
})
.state('component2', {
url: "/component2",
template: "<component-two></component-two>"
})
});
While this is working fine, I have components with arround ten bindings which makes the configuration of the ui-router realy awkward.
I tried using the component property but this doesn't work for me at all. The only other solution I found is to specify the parent using the require property and omit the bindings - but this doesn't feel right for me... Is there a better way to do this?
Here is a plnkr.
UI-Router component: routing exists in UI-Router 1.0+ (currently at 1.0.0-beta.1)
Here's an updated plunker: https://plnkr.co/edit/VwhnAvE7uNnvCkrrZ72z?p=preview
Bind static values
To bind static data to a component, use component and a resolve block which returns static data.
$stateProvider.state('component1', {
url: "/component1",
component: 'componentOne',
resolve: { name: () => 'foo', age: () => 40 }
})
Bind async values
To bind async values, use a resolve which returns promises for data. Note that one resolve can depend on a different resolve:
$stateProvider.state('component1Async', {
url: "/component1Async",
component: "componentOne",
resolve: {
data: ($http) => $http.get('asyncFooData.json').then(resp => resp.data),
name: (data) => data.name,
age: (data) => data.age
}
});
Bind lots of values
You mention you have 10 bindings on a component. Depending on the structure of the data you're binding, you can use JavaScript to construct the resolve block (it's "just javascript" after all)
var component2State = {
name: 'component2',
url: '/component2',
component: 'componentTwo',
resolve: {
data: ($http) => $http.get('asyncBarData.json').then(resp => resp.data)
}
}
function addResolve(key) {
component2State.resolve[key] = ((data) => data[key]);
}
['foo', 'bar', 'baz', 'qux', 'quux'].forEach(addResolve);
$stateProvider.state(component2State);
Alternatively, you can move your bindings a level down and create an object which will be the only bindings. If 10 bindings is what is bothering you.
One alternative you can try is to override the template by custom properties of states in $stateChangeStart event.
Run block like this to achieve this kind of behaviour.
app.run(function($rootScope){
//listen to $stateChangeStart
$rootScope.$on("$stateChangeStart",function(event, toState, toParams, fromState, fromParams, options){
//if the component attribute is set, override the template
if(toState.component){
//create element based on the component name
var ele = angular.element(document.createElement(camelCaseToDash(toState.component)));
//if there is any binding, add them to the element's attributes.
if(toState.componentBindings){
angular.forEach(toState.componentBindings,function(value,key){
ele.attr(key,value)
})
}
//you may also do something like getting bindings from toParams here
//override the template of state
toState.template = ele[0].outerHTML;
}
})
//convert camel case string to dash case
function camelCaseToDash(name) {
return name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}
})
And with this now you can have component property in your state config.
app.config(function($stateProvider, $urlRouterProvider ){
$stateProvider
.state('component1', {
url: "/component1",
component:"componentOne",
componentBindings:{
name:"foo",
age:"40",
}
})
.state('component2', {
url: "/component2",
component:"componentTwo",
})
});
Here is the working plunker.
Still you may have a large config function, but it will look not so awkward.

AngularJS - Everything depends on my login service?

New to Angular here.
I've created a login service, such that when a user logs in, I store things like user name, email, ID, profile picture, etc. within a hash.
Other controllers, can retrieve this information by adding a dependency for this login service, and then accessing the correct property. Example
function MyController(loginservice) {
this.getUsername = function() {
return loginService.userData.userName;
}
this.getUserId = function() {
return loginService.userData.userId;
}
this.getProfilePictureUrl = function() {
return loginService.userData.profilePictureUrl;
}
}
And this works fine... However pretty much every directive and every component and every page is now depending on the loginservice, because they need that info in some form or another.
I suppose an alternative approach is to make the components themselves agnostic of the loginservice, and simply pass the required data as attributes. E.g.
<my-directive username="myController.getUsername()" userId="myController.getUserId()">
</my-directive>
<my-profile picturePath="myControllere.getProfilePicUrl()" username="myController.getUsername()" userId="myController.getUserId()">
</my-directive>
However, this seems a bit overkill.
Any thoughts here?
You are really overcomplicating things by making functions and element attributes for each property of the userData.
If you need the userData in controller just assign the whole object once. Then in view you can add the applicable properties needed to display
In controller or directive:
this.user = loginService.userData
In view:
My Name is {{myController.user.userName}}
<img ng-src="{{myController.user.profilePictureUrl}}">
Or:
<my-directive user="myController.user"></my-directive>
As noted above in comments you can easily inject the user service into the directive also and avoid having to create the same scope item in controller and directive attributes and then in directive scope.
Additionally none of those getter functions you have would be created in the controller, rather they would be in the service so they would also be made available to other components. The structure shown in question is backwards
For example from your current controller code:
this.getUserId = function() {
return loginService.userData.userId;
}
Should really look more like:
this.userId = loginService.getUserId();//method defined in service
I've got a similar setup. I'd recommend using ui-router resolutions, which you can then use to resolve dependencies like user data at the parent, then access in child controllers and views.
There are two key points here:
1) To access 'user' data in child views, you can simply reference the object in parent scope.
2) To access 'user' data in child controllers, you can inject the resolve object.
Here's my setup. This is an example of scenario 1 - accessing data from a child view:
// the root state with core dependencies for injection in child states
.state('root', {
url: "/",
templateUrl: "components/common/nav/nav.html",
controller: "NavigationController as nav",
resolve: {
user: ['user_service', '$state', '$mixpanel',
function(user_service, $state, $mixpanel) {
return user_service.getProfile()
.success(function(response) {
if (response.message) {
$state.go('logout', { message: String(response.message) });
}
if (response.key) {
$mixpanel.people.set({
"$name": response.profile.name,
"$email": response.profile.email
});
}
})
.error(function(response) {
$state.go('logout', { message: String(response.message) });
});
}]
}
})
In my NavigationController, I can define scope to allow child views to access 'user' like so:
function NavigationController($auth, user) {
if ($auth.isAuthenticated()) {
this.user = user.data; // reference 'this' by using 'nav' from 'NavigationController as nav' declaration in the config state - * nav.user is also usable in child views *
}
}
Then you specify the 'root' state as the parent, such as:
.state('welcome', {
parent: "root",
url: "^/welcome?invite_code",
templateUrl: "components/common/welcome/welcome.html",
controller: "WelcomeController as welcome"
})
As for scenario 2, injecting 'user' into a controller, now that the 'welcome' state is a child of the 'root' state, I can inject 'user' into my WelcomeController:
function WelcomeController(user) {
var user_profile = user.data;
}

Is there any way to get a list of child states in angular-ui-router?

I am building an angular app and for part of the app, I want to have a menu with options dynamically populated based on available states. Here is a simplified piece of my state config:
$stateProvider.state('root.type.list.controls', {
url: '/controls',
templateUrl: 'views/controls.html',
controller: 'ControlsCtrl',
data: {
title: 'Controls'
}
})
.state('root.type.list.items', {
url: '/items',
templateUrl: 'views/items.html',
controller: 'ItemsCtrl',
data: {
title: 'Items'
}
})
I'd like to be able to populate a dropdown menu with the titles from every immediate child of the 'root.type.list' state, based on what is in the data section of the config. Is there any way to get a list of all child states for a particular state?
Ended up using $state.get() and doing the following:
function _children(stateName) {
return _.filter($state.get(), function(config) {
return stateName === config.name.slice(0, stateName.length) &&
config.name.split('.').length === stateName.split('.').length + 1;
});
}

Categories