Separating UI components and composing them in one view - angularjs

I have read following two articles (https://scotch.io/tutorials/angularjs-best-practices-directory-structure | https://github.com/johnpapa/angular-styleguide) from which I was convinced to structure my angular project in this way:
-app
---components
------header
---------header.controller.js
---------header.view.html
------personaldata
---------personaldata.controller.js
---------personaldata.view.html
------history
---------history.controller.js
---------history.view.html
------app.module.js
------app.routes.js
---shared
-index.html
(and some more but not important for my question)
Basically I want to create independent components with own controllers, services, directives and so on to place them on multiple, different pages/views. I am currently thinking that the respective view.html files will just include ...lets say...a div tag with some pure html content. Additionally I have a "main page/view" where all these components should be placed on.
How can I achieve that with AngularJS?
My research here brought me to the angular-ui-router prject which allows multiple nested views. Is that really the way to go for. The examples did not seem promising so far as they are loading partials there and I dont see a controller for each partial/component being loaded.
Any help pointing me in the right direction would be greatly appreciated.
Hope the question is clear. A small example putting two of such components in an index.html would as well be great.
Thanks in advance.
Cheers
SLi

I really like this approach and using it on my projects also.
You have still options for composing htmls. You can either write directives for each components so every components can have independencies but the problem is you may want to share same data between two components so this can limit your movement and force you to write much more code then you expect.
The other way is as you mention using nested views feature of ui-router. Let me give you basic example for it...
$stateProvider
.state('app', {
url: '/',
views: {
'content-left': {
controller: 'Ex1Ctrl as vm',
templateUrl: 'example1.html'
},
'content-right': {
controller: 'Ex2Ctrl as vm',
templateUrl: 'example2.html'
}
'content-top': {
controller: 'Ex3Ctrl as vm',
templateUrl: 'example3.html'
}
}
})
As you see we need three ui-view in main html with the name I give on state. So basically your html should be like this.
<div class="row">
<div class="col-md-12" ui-view="content-top"></div>
</div>
<div class="row">
<div class="col-md-6" ui-view="content-left"></div>
<div class="col-md-6" ui-view="content-right"></div>
</div>

Related

With angular component and ui-router, can a single controller have multiple templates?

In a component I would like to associate templates to CRUD actions, but use a single controller that handles data for all all of them. Does this make sense with angular components, or should I use several components ?
How would the ui-router state configuration look like ?
EDIT:
Components have templates, and ui-router states too. I am confused about the articulation of those 2 concepts.
EDIT 2:
trying to clarify my understanding so far :
component are controller + template + bindings. Controller can be omitted.
states are url + template + controller or url + component. Controller can be omitted.
So it seems that components are taking away some of the responsabilities that use to belong to ui-router.
My goal here:
- url1 --> controller foo + template x;
- url2 --> controller foo + template y;
- url3 --> controller foo + template z;
should I do :
components:
component x --> controller foo + template x;
component y --> controller foo + template y;
component z --> controller foo + template z;
and then routing:
url 1 --> component x
url 2 --> component y
url 3 --> component z
?
EDIT 3:
quote from https://docs.angularjs.org/guide/component :
"In a component-based application, every view is a component"
quote from https://ui-router.github.io/guide/ng1/route-to-component :
"The component model enforces separation of concerns and encapsulation by using an isolate scope. Data from parent scopes can no longer be directly accessed. Instead, it needs to be explicitly wired in"
Yes, it is possible. Your ui-router config would look something like this: (Multiple states having same controllers.)
.state('add', {
url: '/add',
templateUrl: 'templates/add.html',
controller: 'contactCtrl'
})
.state('edit', {
url: '/edit',
templateUrl: 'templates/edit.html',
controller: 'contactCtrl'
})
Here's a working example of having multiple states and templates using same controller.
Edit: You don't have to use components. Why create three different extra components while you can achieve the same thing without them? I would still recommend the approach I mentioned above. Getting the same outcome with lesser code should always be chosen. :)
Quoting Eric Elliot on twitter,
Code is temporary. It exists while it is useful.
If code is replaced by better code, good!
If code can be deleted, celebrate!
Your state provider will look like this
JS code
$stateProvider
.state('report', {
views: {
'filters': {
templateUrl: 'report-filters.html',
controller: 'ctrl'
},
'tabledata': {
templateUrl: 'report-table.html',
controller: 'ctrl'
},
'graph': {
templateUrl: 'report-graph.html',
controller: 'ctrl'
}
}
})
in single state you can load multiple views
HTML
<body>
<div ui-view="filters"></div>
<div ui-view="tabledata"></div>
<div ui-view="graph"></div>
</body>
refer multiple views
angular.module('', [])
// Route handler touches this
.component('route1', {
template: `<shared-behaviour>
<route-view-1></route-view-1>
</shared-behaviour>`
})
// This is a purely visual component that links actions with the shared behaviour component
.component('routeView1', {
require: {
shared: '^sharedBehaviour'
},
template: '<button ng-click="$ctrl.onClick()></button>',
controller: class RouteView2Ctrl {
onClick() {
this.shared.onSharedClick()
}
}
})
// Contains the shared behaviour that is shared across the multiple routes
.component('sharedBehaviour', {
// Tell Angular to render children passed to this component
transclude: true,
template: '<div ng-transclude></div>',
controller: class SharedBehaviourCtrl {
onSharedClick() {
/// do something
}
}
})
// Newest version of UI-Router
.state('route1', {
component: 'route1'
})
Further to our comments, you could use something like the above. It's pretty verbose, but that's Angular 1.5 for you. Haven't tested that it works, but it's a basic idea of how it might work.
The main take away being that you have the route1 component which only handles linking route stuff (like the url, resolves, etc) and passes that information to its children (of which there aren't any).
routeView1 just renders how the route would work, and uses require to talk to some parent API that is shared (you could also use one-way bindings for this if you wanted, but with lots of methods this leads to a messy template).
sharedBehaviour only contains the shared behaviour you want renders any child passed to it.
This is admittedly pretty messy and I would prefer that you instead used one-way bindings for route-view-1 and handled the linking in shared-behaviour, but this might be quite a bit of repetition. You could use some transclusion magic to solve that, but it's.. not really worth it.
I would really recommend against sharing controllers like other answerers have suggested. In Angular 2 and above, a controller is the component. You share behaviour there by doing something similar to what I have done (or by using one-way bindings with callbacks) and using composition.
BTW as mentioned in my answer, newer versions of UI-Router for Angular 1.5 allow you to specify a component to be rendered rather than a template. See here for more info.

How to target specific ui-view elements when nested ui-views exist

Imagine some html as follows:
<body ng-app="blocksApp">
Some content goes here
<div ui-view="monty">
<div ui-view="dave">Aaa</div>
<div ui-view="pete">Bbb</div>
<div ui-view="steve">Ccc</div>
</div>
</body>
Using ui-router, is there any way to code a state that will set "dave" to a new snippet of html, whilst leaving everything else untouched.
e.g. I'd like to do this:
$stateProvider
.state('daveonly',{
url: "/dave",
views:{
'dave':{template:"Dave now has content"}
}
})
I can't get it to work. The reason I want to do this is that sometimes I'd like to replace 'Dave' with a partial update, other times I'd like to replace the entire 'monty' with a partial update. It seems that ui-router does not like having nested ui-views in the same snippet of html.
From one point of view I'd like to suggest:
move html code to '.tpl.html' files
use 'templateUrl' instead of 'template'
And check if the following is suitable for you:
$stateProvider.state("daveonly", {
views: {
"dave": {
templateUrl: 'daveonly.tpl.html',
},
"pete": {
templateUrl: 'pete.tpl.html',
},
"steve": {
templateUrl: 'steve.tpl.html',
},
}
});
Take a look at page1 and page2 for more details.
But from another point of view it could be more useful to use only one ui-view and to redesign current ui-views to become the appropriate directives with controllers/services: usage of directives with controllers/services could help to manage partial reload correctly and to write unit-tests.
Yes it can be done easily with the help of abstract states and yes you are correct ui-router doesn't like direct nested views directly but it works fine if the views are in any child template.
Now consider this main page(index.html)
<body ng-app="app">
<div ng-view=""></div>
</body>
Now this template which will appear in this unnamed view. (parent.html)
<h3>This is the parent template</h3>
<div ng-view="child1"></div>
<div ng-view="child2"></div>
Now the JS file
$stateProvide.state('home',{
url:'/',
abstract:true,
views:{
"":{
templateUrl:'parent.html'
}
}
})
.state('home.child',{
url:"",
views:{
'child1#home':{
templateUrl:'child1.html'
},
'child2#home':{
template:'Child2'
}
}
})
.state('home.child.child1',{
url:"child1#home.child",
views:{
'child1#home':{
templateUrl:'child1viewchange.html'
}
}
});
(Now the manipulation part)
(child1.html)
<button ui-sref="home.child.child1">Child</button>
Now child1viewchange.html pe jana padega and wo dekhne wali hai kaunsi kaisi thi/......
(child1viewchange.html)
<h3>Child1's view change</h3>
So now when we click on the button in child view1 the content in the first view changes and if we assign controllers then they can use them to control data.

How to implement two panel layout using Angular Ui-Router?

This is my code
app.config(['$stateProvider', function($stateProvider){
$stateProvider
.state('student',{
url: '/student',
views: {
'list': {
templateUrl: 'list.html',
controller: 'StudentsCtrl'
},
'edit':{
templateUrl: 'edit.html',
controller: 'StudentEditCtrl'
}
}
})
}]);
<html ng-app="app">
<div ui-view="list"></div>
<div ui-view="edit"></div>
</html>
I have layout with two panels side by side,i tried the above code but at the time of page loading two panels html pages displayed at a time. But i want to display first list.html in left side then user clicks add or edit buttons in list.html that time render the edit.html in right side of the panel.
Any reason why you can't combine the HTML and controller together? The StudentCtrl could toggle showing the edit html when the add or edit buttons are clicked.
I can't think of a simple way to make it work with how you have it designed now. You'd have to some coordinating controller in your main HTML and/or some sort of shared Angular service which seems messy.

Angular ui-router parallel view with child states

I've been looking around and perhaps I'm missing something, but I've been totally unable to figure out how to get this working. Basically, I want a state with three parallel views. Lets call them header, body, and footer. Header and footer work just fine as simple parallel views, but I haven't been able to figure out how to automatically render the body child state, so that I can use it to manage other views.
app.js
.state('main', {
url: '/',
views: {
mainModule: { templateUrl: 'partials/main.html'},
"header#main": {
templateUrl: "partials/header.html",
},
"footer#orders": {
templateUrl: "partials/footer.html",
},
}
})
.state('main.body',{
url:'/',
template:"<p>Test!</p>"
})
main.html
<div ui-view="header"></div>
<div ui-view></div>
<div ui-view="footer"></div>
I have a feeling that the ui-view section of the html is not the way to go, and that the answer might have to do with abstract states, but thus far I haven't managed to get it working. Any help would be appreciated.
I have also attempted to reference a view as if it were a state, but that also rendered nothing.
This answer seems to come close, but I haven't been able to get what is suggested in the comments working.
I've looked at other questions that are layout related, but none of the solutions I've come across have worked for me. Thanks!
There is a working plunker, showing all the small adjustments described below in action.
Firstly, we must be sure, that our index.html, the root view, has the place for our 'mainModule' view template. So this is a snippet of the index.html:
<body>
<div ui-view="mainModule" ></div>
...
That, means, that our core view (one of views defined in 'main' state) will be properly injected into root view.
Now, our header and footer should both be using absolute names, but their suffix must be 'main' (not 'orders' like above for footer). That is saying to UI-Router: place them inside of views defined in this state (main.html)
Also, we can (as part of this 'main' state) define some default content of the "body". It could be let's say some list view...
.state('main', {
url: '/',
views: {
// it could be "mainModule" as well... so it needs its: ui-view="mainModule"
mainModule: { templateUrl: 'tpl.main.html'},
"header#main": {
...
},
// wrong absolute name
// "footer#orders": {
// we need the 'main' as well
"footer#main": {
...
},
// even the body, unnamed view could have some default
"#main": {
templateUrl: "tpl.list.html", // e.g. list
},
}
Next, we can defined few more states as children of the 'main'. They will (by default) use the unnamed view of the main. And what's more - replace the list view used above:
.state('main.body',{
url:'/body',
...
})
.state('main.detail',{
url:'/detail:/id',
...
})
Observe it here, it should give all the answers...
The name of the views used in the ui-view directive should match the view names defined in your route configuration section:
<div ui-view="mainModule"></div>
<div ui-view="header#main"></div>
<div ui-view="footer#orders"></div>
I'm not entirely sure if the "#" symbol will give you trouble - if it does, try removing it from the view names.

Nested views without nested states

Following this tutorial: https://github.com/angular-ui/ui-router/wiki/Multiple-Named-Views, i'm able to create an application with multiple views defined in a root template. I need to modify that scheme a bit by putting tabledata and graph to a child view called content. I want my views to look like that:
index.html
<body>
<div ui-view="header"></div>
<div ui-view="content"></div>
</body>
content.html
<div ui-view="tabledata"></div>
<div ui-view="graph"></div>
And my routes looks like that:
# ...
.state('videos',
url: '/videos'
templateUrl: 'content.html'
views:
'tabledata':
templateUrl: 'tabledata.html'
controller: '...'
'sidebar':
templateUrl: 'graph.html'
controller: '...'
)
However, when pointing my browser to /videos, tabledata.html and graph.html templates are not loaded to corresponding views. Everything works great though, if i'll put tabledata and graph views to index.html
I'm sure there is something really wrong with my code but i'm not able to figure out what exactly nor google anything up.
As far as I'm aware you can only have multiple ui-view's in multiple named views, i.e.when you explicitly declare a views property on your state definition. I'm not entirely sure what you're after, but if you would like to have control over where hese ui-views load their states then you can use an abstract state, from the link you provided:
Views override state's template properties
If you define a views object, your state's templateUrl, template and
templateProvider will be ignored. So in the case that you need a
parent layout of these views, you can define an abstract state that
contains a template, and a child state under the layout state that
contains the 'views' object.
This is what I suggest:
.state('videos',{
url: '/videos',
templateUrl: 'content.html',
abstract: true})
.state('videos.xyz',{
url: '/xyz',//you can leave this empty if you like
{
views:{
'tabledata':{
templateUrl: 'tabledata.html'
controller: '...'
},
'sidebar':{
templateUrl: 'graph.html'
controller: '...'
}
}
})
If you don't want that xyz appended to your url's, just pass in an empty string for the url property of the state videos.xyz. I use this approach all the time, let me know if it's what you're after.

Resources