angular / router

The Angular 1 Component Router
MIT License
665 stars 135 forks source link

feat: ability to nest component files in a tree structure #127

Open jpzwarte opened 9 years ago

jpzwarte commented 9 years ago
/app
  /components
    /app
      app.js
      app.html
    /dashboard
      dashboard.js
      dashboard.html
      /results
        results.js
        results.html
      /lessons
        lessons.js
        lessons.html

// in app.js
$router.config([
    { path: '/dashboard', component: 'dashboard' }
])

// in dashboard.js
$router.config([
    { path: '/', redirectTo: '/results' },
    { path: '/results', component: 'results' },
    { path: '/lessons', component: 'lessons' },
])

However, the router looks for the results.html template in app/components/results instead of app/components/dashboard/results

tobidelius commented 9 years ago

wouldn't this tend to become very very nested when you have many components?

0x-r4bbit commented 9 years ago

I know that @btford wanted to write something down or sth., on why it's good to have a flat component tree. However, AFAIK (at least from what I've seen in the WIP docs) there will be support for nesting components.

Just unclear what exactly that means, I hope Brian will shed some light into darkness when he finds time :)

jgoux commented 9 years ago

This is great, I'm in favor of nested components if they aren't reusable elsewhere. I really like this approach from ryan florence : https://gist.github.com/ryanflorence/daafb1e3cb8ad740b346#shared-modules

jrgleason commented 9 years ago

I am in need of this as well. I use HTML2JS to convert all of my templates into JS. I also use the Maven structure so my project structure looks like this...

src
    main
        templates
            components

Until this is considered I guess my work around is a custom $componentLoaderProvider()? Does anyone know how I would override the provider?

schmitch commented 9 years ago

I have a quick fix for that:

app.config(['$componentLoaderProvider', function($componentLoaderProvider) {
    function dashCase(str) {
      return str.replace(/([A-Z])/g, function ($1) {
        return '-' + $1.toLowerCase();
      });
    }

    $componentLoaderProvider.setTemplateMapping(function (name) {
      if (name.indexOf('/') !== -1) {
        var part = name.split('/');
        var dashName = dashCase(part[1]);
        return './components/' + dashCase(part[0]) + '/components/' + dashName + '/' + dashName + '.html';
      } else {
        var dashName = dashCase(name);
        return './components/' + dashName + '/' + dashName + '.html';
      }
    });

    $componentLoaderProvider.setCtrlNameMapping(function(name) {
         if (name.indexOf('/') !== -1) {
           var part = name.split('/');
           return part[1][0].toUpperCase() + part[1].substr(1) + 'Controller';
         } else {
           return name[0].toUpperCase() + name.substr(1) + 'Controller';
         }
    });
}])

Currently it only supports 1 nesting but its easy to make that bigger. Also you need to specify the component as mainComponent/subComponent

EDIT: Oh and it isn't the best way to do it, since ng-link='' can't work with that model.

jvandemo commented 9 years ago

Here are some of the pro's and cons I can think of for both flat and recursive strategies:

Flat strategy

Recursive strategy

Other considerations

schmitch commented 9 years ago

there is one thing missing Flat has a Con that it could be really messy if we have a really big application. Consider the Dashboard of GCP as an example. Also my company is developing a really big application in angularjs, the new router with a flat structure would be really really ugly.

Flat is better for mobile first, since most of the time a mobile application doesn't need to have everything. While recursive is better in the long run / overall.

jvandemo commented 9 years ago

@schmitch I have added the CON to the list. Thanks for the addition.

We have been employing a flat structure in approximately 10 different apps for clients that past year (one of which is a very large one). The maximum number of components we had to use was 40 and the flat overview was still very clear.

We use components to structure the project by feature. One component can contain its own services, directives, filters, styles, assets, etc.

For example a single component 'users' contains:

I can't judge your situation but do you think your application requires an even larger amount of components?

(I'm totally not judging here, just trying to learn from your situation).

schmitch commented 9 years ago

Hm... Our application doesn't have so many components.

We currently grouped everything related together.

Cosidering a Small CRM, which is a component inside an even bigger application. And currently the components have the following "components":

Each of these will have a Controller, directive's (not that many yet), templates, services. (We don't group styles, yet..)

So how should somebody organize that?

When it comes to your approach everything is a feature so you would have 3 components, but that's odd, since you only can query the address list after you selected the user and you can only create a new contact, after you already clicked the customer.

And in the long run there will be more things that are related to the component 'customer'.

So I don't think flat is good, since you will split related parts away from each other. Maybe I'm thinking wrong but for me the subrouter part was a really good design so that you have a subrouter which will have all the routes that needs to be set for a component and all of his related parts that lives inside the same 'component', 'namespace' or call it how you want.

jgoux commented 9 years ago

I personnaly share @schmitch vision, I also nest my components. In my last project, everything is a component, I use only directives + services. It's really important to me to be able to see the dependency tree of my components, so the folder structure help me in this purpose. And it also reflect my routes structure, so I can think of my application in term of "screens". I couldn't with a flat structure. But it's like all, I think it's a matter of preference, nobody is wrong, do what best fit your needs.

Edit : So in conclusion, the router should handle both cases. :D

btford commented 9 years ago

I'm all for this – I'll be looking into how best to implement it soon.

jrgleason commented 9 years ago

I agree that both should be supported I think that although flat may be the "prefered current standard" businesses do not typically react that fast and so large project refactors are probably fairly untenable (unless there is data to prove me wrong). I am trying to be very positive about 2 but trying to force people into one structure or another limits audience and that means my labor will be more expensive.

I will be interested in seeing alternatives maybe a regex attr like forEach?

On Wed, Mar 11, 2015 at 5:06 PM, Brian Ford notifications@github.com wrote:

I'm all for this – I'll be looking into how best to implement it soon.

— Reply to this email directly or view it on GitHub https://github.com/angular/router/issues/127#issuecomment-78371627.

darcnite3000 commented 9 years ago

Bringing my discussion over from #165 we really need the $componentLoader to be more easily configured, i don't like the idea having to recreate the dasherize functionality just to modify the root folder for my components.

jvandemo commented 9 years ago

Bringing over my proposal from #160 (closed duplicate issue).

Current situation

Currently the component loader resolves the component controller and component template using the $componentLoader service.

We feed the component name into the service and get an object with the controller name and template to instantiate the component:

{
  controllerName: 'SampleController',
  template: './components/sample/sample.html'
}

Using the $componentLoaderProvider we can overwrite the resolving functions to change the strategy globally to our liking.

Drawback

The resolution strategy is global where individual components may use different strategies (ideally not, but realistically this will probably happen).

Proposal

We could allow the controller name and template to be passed to the route config directly to "overwrite" the default behavior of the component loader:

$router.config([
  {
    path: '/',
    component: {

        // We could use 'as' similar as in routes to
        // specify the name of the component because
        // we are dealing with an object now
        as: 'main',

        // Controller to instantiate as component controller
        controllerName: 'SomeDeviatingController',

        // What name to use to inject controller in template
        controllerAs: 'main',

        // Component template
        template: './components/other-location/template.html'
    }
  }
]);

Implications

This would require the entire instruction to be passed into the $componentLoader service instead of just the component name.

It would also require more parsing of the arguments in the $router.config() method in case an object is received.

Positive side effect

Passing the entire instruction to the component loader allows the loader functions to become more intelligent. Theoretically you could even pass meta data in the instruction (not sure if current implementation would allow this) to make routing decisions.

The default loader functions could then be an implementation of such more intelligent functions that first look whether the controller name and template are literally specified before attempting to apply the convention.

Benefits

Possible benefits:

I think this could add a lot of power to the router, but would definitely need more thought to see if it is technically doable.

Examples

Load component from flat component structure doesn't change:

$router.config([
  {
    path: '/',
    component: 'admin'
  }
]);

Overwriting component defaults manually:

$router.config([
  {
    path: '/',
    component: {

        // We could use 'as' similar as in routes to
        // specify the name of the component because
        // we are dealing with an object now
        as: 'main',

        // Controller to instantiate as component controller
        controllerName: 'SomeDeviatingController',

        // What name to use to inject controller in template
        controllerAs: 'main',

        // Component template
        template: './components/other-location/template.html'
    }
  }
]);

Pass meta data to $componentLoader to customize strategy:

$router.config([
  {
    path: '/',
    component: {

        // We could use 'as' similar as in routes to
        // specify the name of the component because
        // we are dealing with an object now
        as: 'main',

        // Pass meta data som $componentLoader can
        // use this data to decide how to resolve controller
        // and template. Can contain anything you like as
        // it is passed entirely to $componentLoader.
        // Using a custom $componentLoader, you can then
        // use this to apply any strategy you need in your app.
        // Example:
        data: {
          flat: false,
          controllerName: 'CustomController'
        }
    }
  }
]);
nlwillia commented 9 years ago

I think there's a risk of treating components as incidental to routing when they are an important concept on their own. Routing may be the backbone of an application, but components (and hopefully someday "web components") are the building blocks, and we absolutely need the freedom to physically arrange them in whatever way suits the application. There was a bit of churn in Angular 1.x boilerplate/scaffolding conventions and debate over best practice, but I'd hate to see Angular 2 react to that by being too opinionated.

I also wonder if route configuration is the right place to define all the structure of a component. Shouldn't details of where the template is located and what controller function to use be encapsulated within the component itself? Aliasing or configuration would make sense in the router, but I'd expect components, like directives, to have a pretty strong identity on their own. I'd actually like to see more clarity on how a component is (or should be) any different from a directive. Isn't route-driven selection in a viewport analogous to an ng-choose alternating between directives with some extra lifecycle hooks?

dcunited08 commented 9 years ago

@nlwillia I believe the component markup in ng2 handles that for us, in current ng1.x the template for the controller is defined in the router so it is being kept there. I would be interested to see an example on ng2 as davideast/ng2do#1 doesn't have the router in it.

Ng2:


// Component section
@Component({
  selector: 'my-app'
})
@Template({
  inline: '<h1>Hello {{ name }}</h1>'
})
// Component controller
class MyAppComponent {
  constructor() {
    this.name = 'Alice';
  }
}

Ng1:

.config(function($routeProvider, $locationProvider) {
  $routeProvider
   .when('/Book/:bookId', {
    templateUrl: 'book.html',
    controller: 'BookController'
    }
  })
jeffwhelpley commented 9 years ago

Two thoughts. First, I don't think you should allow by default nesting. This can get really confusing and ugly quick. And, it doesn't actually help organizing your code like you think it would. I have gone down this route before and it sucks. For sure keep the structure flat by default.

For large apps, the solution is not to allow nesting but rather allow components to come from external apps/modules/whatever a group of things are called in Angular2. I am still learning Angular2, but at a high level, you should be able to do the equivalent of this from Angular 1.x:

angular.module('myApp', ['myComponents1', 'myComponents2']);

Then the component loader knows to automatically first look in the current local app's components dir and then look at the components within myComponents1 and myComponents2.

nlwillia commented 9 years ago

@dcunited08 That's helpful. However, even though the Ng1 syntax is likely regarded as a bit of a transitional shim, it still stands to be with us for a while. It would be nice if it promoted Ng2 style thinking about application structure in a way that was more functionally equivalent and more conventional to Ng1 developers. A better bridge between the two might be something like angular.component('MyComponent', {...component definition object...}) and then $routeProvider.when('path', 'MyComponent').

dcunited08 commented 9 years ago

@nlwillia Look at angular/angular.js#10007

jgoux commented 9 years ago

@nlwillia This is exactly what I do with ui-router. All my concrete states are mapped to an inline directive. :+1:

jvandemo commented 9 years ago

I also wonder if route configuration is the right place to define all the structure of a component.

I believe so, since the router is actually considered part of the component in Angular 1.x. From the documentation:

In Angular 1, a "routable component" is a template, plus a controller, plus a router.

Because ng-viewport creates a new child router that is injected in the component controller as $router, I think it is safe to assume that $router can be used to store component routing information (because each component has its own $router).

nlwillia commented 9 years ago

@dcunited08 Thanks for the link!

@jvandemo The design document describes components as "re-usable state tree branches that can be attached anywhere easily". If the same template/controller is routable at more than one place in the application, then it's preferable for the repeated characteristics to be encapsulated in a component definition that is separate from the route configuration. In that case, the capabilities discussed in this issue are still important, but they are a higher-level consideration than just $router.

jvandemo commented 9 years ago

@nlwillia — Agree, I think that's what I'm trying to say.

The routing configuration (internal to the component) is part of the component itself.

The component's routing configuration is defined in the component's controller using the $router that is injected at runtime and re-used everywhere the component is used.

If you take away the component, the routing configuration is removed as well.

If you use the same component multiple times in different viewports, its routing configuration is added multiple times, but you only defined it once (in the component controller).

Does that make sense? Or did I misunderstand your point? Thanks!

nlwillia commented 9 years ago

@jvandemo The point I was trying to make is that a component's template and controller are implementation details that should be opaque at the point where routing to an instance of that component is configured. (Route parameters muddy the water a little, but part of route configuration could be binding the actual parameter name to a formal property name documented as expected by the component.) The scenario of a component encapsulating the relative routing configuration of composed sub-components is fine, of course.

jvandemo commented 9 years ago

@nlwillia — I think I understandig where you're getting at. So instead of defining routing configuration inside a component's controller, you could actually e.g. attach it as a property to the controller, so the definition is no longer part of the controller code.

Am I getting that right?

nlwillia commented 9 years ago

@jvandemo I think the overall $router.config pattern is fine, but I don't want to be defining components (template + controller) inline in the route definition as you had proposed above. Those details should be encapsulated within the component and the component itself referenced abstractly. The issue that dcunited08 linked seems to be on the right track.

sorenhoyer commented 9 years ago

I was about to consider transitioning from ui-router to this new angular router, but since my CMS is already developed in modules, I cannot use this router without some really ugly hacks:

Consider this example structure of a CMS:

cms
cms/core
cms/core/public/app.js (dynamically loads all routes etc)

cms/core/modules/module1/public/
cms/core/modules/module1/public/ng-controllers/
cms/core/modules/module1/public/ng-services/
cms/core/modules/module1/public/ng-views/...

cms/core/modules/module2/public/...`

As I see it this current approach of "forcing" us to use a best-practice predefined components directory structure only take into account the very basic and simple use case - the self-contained angular app itself - BUT what if the angular components live in different modules in the main application (which the angular application is merely a part of)? I may be wrong though?

If we must continue with the components concept, maybe allowing us to specify the different components folder locations could be an idea here?

I would however prefer the flexibility of mapping urls directly to template urls

dsnoeck commented 9 years ago

I have the same issue as @sorenhoyer. Please, let us define where the components are located.

Mig1st4ck commented 9 years ago

I always liked angular because it didn't force me to use a fixed folder structure.

I can just set my own and live with it. I have a very large app, and using ui-router right know, my templates live in paths like this.

$templateCache.put("/app/entidades/views/layout.html"
$templateCache.put("/app/entidades/views/main.html"
$templateCache.put("/app/items/views/layout.html"
$templateCache.put("/app/main/views/main.html"
$templateCache.put("/app/reports/views/layout.html"
$templateCache.put("/app/docs/stocks/views/layout.html
$templateCache.put("/app/entidades/produtores/views/details.html
$templateCache.put("/app/reports/views/reportsCreator.html"

So each modules knows where the template is. The new router force us to use a fixed folder structure.

I would like to be abre to just override the templateUrl in the config method.

$router.config([
    { path: '/', redirectTo: '/results' },
    { path: '/report', component: 'reportCreator', templateUrl: "/app/reports/views/reportsCreator.html" },
    { path: '/doc/stocks', component: 'docStocks', templateUrl: "/app/docs/stocks/views/layout.html"},
])

or get the full object in the $componentLoaderProvider#setTemplateMapping This way could just use whatever logic we want in our app.

georgiosd commented 9 years ago

Was there any decision on this?

tiagopromob commented 8 years ago

Was there any decision on this?2

rafalradomski commented 8 years ago

Any news?

rdehuyss commented 8 years ago

+1

sublime392 commented 8 years ago

+1