Configure ui-router with components containing multiple bindings - javascript

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.

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?

Dynamic URLs in angular

I currently Angular 1.6 and angular-ui/ui-router
I have a problem with dynamic url which I don't know a limit of parameter
example
www.example.com/name/hello/address/telephone/postcode/city/ .... (n)
or
www.example.com/name/hello/school/age/weight/height/...(n)
or
www.example.com/name/hello/friends/family/age/address
from the example url I need to using ui-route to manage that url and get value parameters from url and just implement only one state.
expected result
let result = [hello,address,telephone,postcode,city, .... (n)];
example route (*only one state handle this case)
.state('person', {
url: '/name/:param1/:param2', (which I want to dynamic)
component: 'person',
resolve: {
search:($transition$) {
return $transition$.params();
}
}
})
Please advice :)
Update :
I found a good solution from this link
Recursive ui router nested views
you can make a function which takes your array input and return an state object.
function generateState(array){
var url = a.reduce(function(url, item){ return url+":"+item+"/" }, "/name/");
return {
url: url,
component: 'person',
resolve: {
search:($transition$)=> {
return $transition$.params();
}
}
}
}
after which you can dynamically register the state using $stateProvider

Angular 1.6 bindings inside controller

Im trying to pass some parameters to my component through bindings, but unfortunately I'm not having luck in using those params in my controller, this is my code:
angular.module('project1').component('menu', {
templateUrl: '/static/js/templates/menu.template.html',
bindings: {
rid: '#'
},
controller: ['Restaurant', function RestaurantListController(Restaurant) {
console.log(this.rid);
console.log(this);
this.restaurant = Restaurant.get({restaurantId: this.rid});
}]
});
HTML component:
<menu rid="1"></menu>
The interesting thing is that i can access the parameters in the template and when i do the 2 console log, the first one is undefined, but in the second one i can see the rid variable...so, i really don't understand what i'm missing.
With angular 1.6, your bindings are going to be ready on the method $onInit and not before.
If you need to re-enable auto bindings
https://toddmotto.com/angular-1-6-is-here#re-enabling-auto-bindings
If anyone still searching for solution, use $onInit method provided by angular.
this.$onInit = function () {
$http.get(`/api/v1/projects`).then((res) => {
$scope.projects = res.data;
}, (err) => {
$scope.error = err
})
};

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

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;
}

Categories