Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Routing to Angular 1.5 .component() #2627

Closed
christopherthielen opened this issue Mar 17, 2016 · 45 comments
Closed

RFC: Routing to Angular 1.5 .component() #2627

christopherthielen opened this issue Mar 17, 2016 · 45 comments

Comments

@christopherthielen
Copy link
Contributor

I'd like to solicit some feedback from the community about routing to ng1 .component()s.

Backstory

We recently added support for $scope.$resolve to the master and legacy branches. After the next release, you will be able to route to components, and access resolved data using a "component template", similar to ngRoute. Here's a proof of concept: http://plnkr.co/edit/mG9W1q6B4CCHI6QEfKSJ

In angular2, however, we cannot use "component templates", and instead will be routing to a component class. In ui-router-ng2, you will supply the component's class in a state definition, probably something like this:

import {FooComponent} from "./fooComponent.js";
import {UIRouter, StateRegistry} from "ui-router-ng2";

let stateRegistry = uirouter.stateRegistry; // uirouter instance comes from somewhere
stateRegistry.state({
  name: 'foo',
  component: FooComponent, // Reference to the FooComponent class
  url: '/foo/:fooId'
}

I would like both ui-router for ng1 and for ng2 to have a similar look and feel when routing to components. Because of that, I'm considering allowing component: in a state definition for ui-router 1.0 for ng1.

There are a few mechanisms I've been tossing around as options for declaring a state's view should route to a component, and how to provide resolved data to that component. Note that we can't use constructor injection into a .component() to provide resolve data like we would for a normal ui-router template/controller combo, because we can not instantiate and manage the component lifecycle ourselves.


Given this angular 1.5 .component()

app.component('myComponent', {
  templateUrl: 'myComponent.html',
  controller: 'MyController',
  bindings: { input1: '<', input2: '<' }
});

Options

Option 1: Keep it as-is. Use a template: and access data using $resolve.
$stateProvider.state('foo', {
  template: '<my-component input1="$resolve.foo" input2="$resolve.bar"></my-component>',
  url: '/foo/:fooId/:barId',
  resolve: {
    foo: ($stateParams, FooService) => FooService.get($stateParams.fooId),
    bar: ($stateParams, BarService) => BarService.get($stateParams.barId)
  } 
});
Option 2: Route to the component by name.

Require each component bindings to match a resolve name

$stateProvider.state('foo', {
  component: 'myComponent',
  url: '/foo/:fooId/:barId',
  resolve: {
    // input1 and input2 are same names as myComponent `bindings`
    input1: ($stateParams, FooService) => FooService.get($stateParams.fooId),
    input2: ($stateParams, BarService) => BarService.get($stateParams.barId)
  } 
});
Option 3: Route to the component by name + bindings mapping string

Allow resolve names to be mapped to component bindings using a DSL similar to ui-sref
The use case is that resolves may come from parent states, so may need to be mapped to the component bindings

$stateProvider.state('foo', {
  component: 'myComponent({ input1: foo, input2: bar })',
  url: '/foo/:fooId/:barId',
  resolve: {
    foo: ($stateParams, FooService) => FooService.get($stateParams.fooId),
    bar: ($stateParams, BarService) => BarService.get($stateParams.barId)
  } 
});
Option 4: Route to the component by name + bindings mapping object

Allow resolve names to be mapped to component bindings using an object
Again, the use case is that resolves may come from parent states, so may need to be mapped to the component bindings

$stateProvider.state('foo', {
  component: 'myComponent',
  bindings: { input1: "foo", input2: "bar" },
  url: '/foo/:fooId/:barId',
  resolve: {
    foo: ($stateParams, FooService) => FooService.get($stateParams.fooId),
    bar: ($stateParams, BarService) => BarService.get($stateParams.barId)
  } 
});
Option 5: Route to the component by name; Supply resolves via ui-view controller

For this option, the component has to be aware that it's inside of a ui-view. It requires the ui-view controller and accesses the resolves from there. The bindings object is not used.

app.component('myComponent', {
  templateUrl: 'myComponent.html',
  controller: function() {
    this.$onInit = function() {
      this.input1 = this.uiView.$resolve.foo;
      this.input2 = this.uiView.$resolve.bar;
    }
  },
  require: { uiView: '^uiView' }
});

$stateProvider.state('foo', {
  component: 'myComponent',
  url: '/foo/:fooId/:barId',
  resolve: {
    foo: ($stateParams, FooService) => FooService.get($stateParams.fooId),
    bar: ($stateParams, BarService) => BarService.get($stateParams.barId)
  } 
});

Option 6: Your option
@christopherthielen christopherthielen changed the title RFC: Routing to Angular 1.5 .component() and Angular 2 Component RFC: Routing to Angular 1.5 .component() Mar 18, 2016
@brandonroberts
Copy link

I like option 2. It aligns more with how the components are dynamically loaded in Angular 2. How would these resolved values get set for the state component?

@timjacobi
Copy link

Option 1

This is what i'd go for. The UI router API is pretty comprehensive already so I wouldn't feel comfortable adding more things.

Option 2

I feel like there would be too much magic going on, especially when someone else picks up the code and doesn't know that resolves are autobound to the component's bindings

Option 3

Not a fan of micro DSLs unless they're unavoidable

Option 4

This looks to me like a better version of Option 2 but I still prefer Option 1

Option 5

Least favourable option in my opinion as it ties a component to UI router which renders it more or less unusable in other contexts which is the idea of a component

@luaks
Copy link

luaks commented Mar 18, 2016

I'm mostly leaning towards options 2 and 4. My thought would be to default to the syntax of option 2 but provide the bindings from option 4 as a mapping syntax for when it might be necessary.

@rogerpadilla
Copy link

Option 2, and it is (should) be compatible (extensible) with Option 4.

@christopherthielen
Copy link
Contributor Author

How would these resolved values get set for the state component?

@brandonroberts much like the plunker above (see: "routeToComponent.js")

  • Process the resolves (async) during the transition.
  • Inspect the component's DDO for bindToController values
  • Build a ui-router template/controller for the view.
    • The controller instance gets the resolved values as $resolve
    • The generated template is a "component template" such as <my-component input1="::$ctrl.$resolve.input1"></my-component>
  • Angular compiles the template and one-time binds the resolve data to the directive

@christopherthielen
Copy link
Contributor Author

@luciano-spinn3r I'm having trouble understanding all of your points, but it sounds like you are advocating for a drastic architecture change, along the lines of the component router.

That's not on the table here; ui-router will continue to use the state tree abstraction to define the application structure and data retrieval/lifecycle. Components are applied as views.

Feel free to correct me if I missed some nuance of your suggestion. However, I don't want to derail the initial RFC.

@PabloHidalgo
Copy link

I was laking this functionality, as I'm looking forward to achieve a full angular app architecture based on routable components (just to test it vs a classical one) and balance the benefits and disadvantages of working with this flow.

I'm my opinion, option 2 as default with optional 4 (more flexible, would override default - good point on parent resolves!) should be the way.

Thanks for all and keep up the good work!

@eddiemonge
Copy link
Contributor

Option 2 looks good to me

@wesleycho
Copy link
Contributor

I think option 4 looks the best, with sensible default (bindings can be omitted with a default to an empty object).

@christopherthielen
Copy link
Contributor Author

with sensible default (bindings can be omitted with a default to an empty object).

Do you mean you prefer by default, that no automatic bindings would occur like option 2?

@wesleycho
Copy link
Contributor

Yup

@DmitryEfimenko
Copy link

option 2 as default with optional 4

@felixfbecker
Copy link

1 should be supported, and 4 would be nice as an addition.

@gaui
Copy link

gaui commented Mar 20, 2016

My vote goes to option 2.

@gnomeontherun
Copy link

I think 1 should be supported just to be related to the ngRoute syntax. However, I like 2 as the default. It makes sense that you can provide the bindings through a resolve, though it is possible that a binding might not be required, so it should not get upset by not providing a binding and default to null.

@christopherthielen
Copy link
Contributor Author

Done... options 1, 2, and 4 are supported.

This works for:

  • angular 1.5 .component()
  • angular 1.3+ and Todd Motto's .component() polyfill
  • angular 1.3+ bindToController directive
  • angular 1.2+ directive with scope: { foo: '=' } bindings

This code works by decorating states that have component: 'foo' with a templateProvider that builds a template that looks something like <foo my-data="::$resolve.myData"></foo>

@samithaf
Copy link

samithaf commented Apr 28, 2016

Nice work!. All seems to be fine fine with Angular 1.5 except it seems $stateChangeError& $stateChangeStartno longer fires. Please note that I have imported stateEvents to project as well. I am using 1.0.0-alpha.3 of ui-router. However $transitions.onError etc fires as expected.

@christopherthielen
Copy link
Contributor Author

@samithaf did you add the angular module dependency to "ui.router.events" as well?

@samithaf
Copy link

samithaf commented Apr 28, 2016

hey @christopherthielen thanks for getting back and the hint. Wonder module name is ui.router.state.events? I just checked the source of stateEvents.js. When I add the ui.router.state.events as the dependancy everything started working. One request, could you please export name of the Angular module in stateEvents.js? Then people can do something like,

import stateEvents from 'angular-ui-router/release/stateEvents';

.module('my.app', [
stateEvents

@christopherthielen
Copy link
Contributor Author

Yes! Whoops.
On Thu, Apr 28, 2016 at 4:56 PM Samitha Fernando notifications@github.com
wrote:

hey @christopherthielen https://github.com/christopherthielen thanks
for getting back. Wonder module name is ui.router.state.events? I just
checked the source of soruceEvents.js


You are receiving this because you were mentioned.

Reply to this email directly or view it on GitHub
#2627 (comment)

@CKGrafico
Copy link

Amazing work, thanks :D

@mgamsjager
Copy link

How would one implement lazyloading with the native 1.5 components?

@fpipita
Copy link
Contributor

fpipita commented May 3, 2016

This is awesome, thanks a lot!

Question: is there a way to use options 2 or 4 to route to a ng1 component that also exposes some outputs (& bindings)?

For example:

module.component(`foo', {
  template: `<a href ng-click="$ctrl.output()">click me {{ $ctrl.input }}</a>`,
  controller: function() {},
  bindings: {
    input: `<`,
    output: `&`
  }
})
.config($stateProvider => {
  $stateProvider.state(`foo`, {
    url: `/foo`,
    component: `foo`,
    resolve: {
      input: () => `Francesco`,
      output: () => () => alert(`Clicked!`)
    }
  });
});

@christopherthielen
Copy link
Contributor Author

How would one implement lazyloading with the native 1.5 components?

@mgamsjager

Lazy load components

Here's an example of a lazy loading hook: http://plnkr.co/edit/3kvrVY?p=preview

@mgamsjager
Copy link

@christopherthielen
Thanks for the example.

@xeii
Copy link

xeii commented May 6, 2016

I have a question about the component -syntax. I have a parent component, and N-number of child components, which require data resolved by parent component:

.component('parentComponent', {
    bindings: {
      input1: '<',
      input2: '<'
    }
  })
.component('childComponent', {
    controller: ChildController,
    template: ChildTemplate,
    require: {
      parentCtrl: '^parentComponent'
    }
  })

And in the state declarations I want to use the nice new component syntax:

.state('parent', {
  abstract: true,
  views: {
    '@': {
       component: 'parentComponent',
    }
  },
  resolve: {
    input1: () =>  data1
    input2: () =>  data2
  }
.state('parent.child, {
  component: 'childComponent'
})

Now the problem is that parentComponent is lacking ui-view, and the child does not render. With the template syntax I used this method:

template: '<parent-component ' +
  '  input1="$resolve.input1"' +
  '  input2="$resolve.input2" ui-view>' +
  '</parent-component>',

This is what I am using now (extend extra div in component declaration):

.component('parentComponent', {
    template: '<div ui-view></div>',
    bindings: {
      input1: '<',
      input2: '<'
    }
  })

Is there any way to append ui-view to the template generated by the component syntax? I can make a sepparate issue if it required.

@christopherthielen
Copy link
Contributor Author

@fpipita Interesting idea... can you provide me with a concrete example of how you might use the & binding?

I'm considering allowing any type of component binding, but I'm struggling with a compelling or meaningful use case for "&" binding from a resolve. Would the callback be defined on a parent state, or the same state? Would it interact with other resolves? Respond to UI events?

I'm wondering if the intention is to get some "smart component" functionality? Would it be simpler to introduce a "smart component" that wraps your other component, and route to that instead?

@christopherthielen
Copy link
Contributor Author

christopherthielen commented May 7, 2016

@xeii I don't quite understand all the pieces you've shown

What else does your component do? Why do you bind the resolve data to the component, if it has no template or controller? It might be easier to just use a standard state definition with a template: <ui-view></ui-view>


In general, I feel like this is a pretty different than how most people will use route-to-component.

I suggest explicitly adding the ui-view to your template, like you demonstrated (because it's the simplest, and least surprising to others). However, if you really want to put a ui-view on all route-to-components, it is certainly possible implement your own custom views decorator.

http://plnkr.co/edit/xZhaQd?p=preview

This views: decorator will generate <foo ui-view></foo> or <foo input="::$resolve.input" ui-view></foo>

  $stateProvider.decorator("views", (state, parentFn) => {
    // Call the parent views: builder. 
    // This thing takes the state, converts the its views: declaration
    // into the internal `views` representation object
    let viewsObj = parentFn(state);

    angular.forEach(viewsObj, (view, key) => {
      if (view.component) {
        // it has a component: declaration... let's 
        // decorate the results of the real template provider

        // First, save a copy of the stock ui-router templateProvider
        let realTemplateProvider = view.templateProvider
        // Then, override the view's templateProvider with our own
        view.templateProvider = [ "$injector", function($injector) {
          // Invoke the original templateProvider to get the template
          let realTemplate = $injector.invoke(realTemplateProvider);
          console.log('original template: ', realTemplate);
          // Use string replacement to add the ui-view
          let newTemplate =   realTemplate.replace("><", " ui-view><");
          console.log('replaced template: ', newTemplate);
          return newTemplate;
        }];
      }
    });

    return viewsObj;
  })

@xeii
Copy link

xeii commented May 9, 2016

@christopherthielen What I need is the resolved data by the parent component to be accessed in child components, thus this (in child component declaration):

    require: {
      parentCtrl: '^parentComponent'
    }

This is the best aproach I could think of using components. And I really liked the way I could keep the state declarations very clean. The 'parent' here does not need to be a component, like you said it does not have any template.

@lucianogreiner
Copy link

I tried to bring this up for discussion once, but it hasn't been well
accepted/understood. IMHO i don't think it makes sense any component
routing while the parent component component isn't in charge or part of it.
The way i've seen both ui-router and angular-router are doing this makes it
almost impossible to create a real reusable component-tree application.

Luciano Greiner
(51) 82355437

On Mon, May 9, 2016 at 4:00 AM, Antti Väyrynen notifications@github.com
wrote:

@christopherthielen https://github.com/christopherthielen What I need
is the resolved data by the parent component to be accessed in child
components, thus this (in child component declaration):

require: {
  parentCtrl: '^parentComponent'
}

This is the best aproach I could think of using components. And I really
liked the way I could keep the state declarations very clean. The 'parent'
here does not need to be a component, like you said it does not have any
template.


You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub
#2627 (comment)

@christopherthielen
Copy link
Contributor Author

christopherthielen commented May 9, 2016

What I need is the resolved data by the parent component to be accessed in child components, thus this (in child component declaration):

It seems that your parent component doesn't do anything besides encapsulate the resolve data?

A child "route-to-component" can directly bind to resolves from the parent states.

.component('childComponent', {
    controller: ChildController,
    template: ChildTemplate,
    bindings: {
      input1: '<',
      input2: '<'
    }
  })
.state('parent', {
  abstract: true,
  template: '<ui-view></ui-view>'
  },
  resolve: {
    input1: () =>  data1
    input2: () =>  data2
  }
.state('parent.child', {
  component: 'childComponent'
})

@christopherthielen
Copy link
Contributor Author

I tried to bring this up for discussion once, but it hasn't been well
accepted/understood.

@lucianogreiner I'm listening.

i don't think it makes sense any component routing while the parent component component isn't in charge or part of it

I don't understand your point though, can you try again?

@xeii
Copy link

xeii commented May 10, 2016

@lucianogreiner

A child "route-to-component" can directly bind to resolves from the parent states.

This works because ui-router uses $scope behind the scenes? Anyway, would be nice to have that example in the docs. And thanks for the responses!

@fpipita
Copy link
Contributor

fpipita commented May 10, 2016

@christopherthielen thanks for your reply.

Interesting idea... can you provide me with a concrete example of how you might use the & binding?

Sure, here you go:

angular
.module(`app`, [`ui.router`])

  .factory(`ItemService`, () => {
    const itemList = [
      {id: 1, name: `computer`},
      {id: 2, name: `mouse`},
      {id: 3, name: `keyboard`}
    ];

    return {
      getAll: () => [...itemList],
      getById: id => itemList.find(i => i.id === id)
    };
  })

  .component(`item-list`, {
    template: `
      <ul>
        <li ng-repeat="item in $ctrl.itemList"
            ng-click="$ctrl.onOpenItem({$event: {item: item}})">
          {{ item.name }}
        </li>
      </ul>`,
    controller: function() {
    },
    bindings: {
      itemList: `<`,
      onOpenItem: `&`
    }
  })

  .component(`item`, {
    template: `<h1>{{ $ctrl.item.name }}</h1>`,
    controller: function() {
    },
    bindings: {
      item: `<`
    }
  })

  .config($stateProvider => {
    $stateProvider

       // here the & binding would come handy because the template 
       // is only needed to inject the callback. 
       // A thing worth noting is that there should be a mechanism
       // that allows to specify how to pass the properties exposed by the component's
       // output on the template to the callback ($event in this case)

      .state(`itemList`, {
        url: ``,
        template: `
          <item-list itemList="$resolve.itemList"
                     on-open-item="$resolve.onOpenItem($event)">
          </item-list>`,
        resolve: {
          itemList: ItemService => ItemService.getAll(),
          onOpenItem: $state => $event => $state.go(`item`, {
            itemId: $event.item.id
          })
        }
      })

      .state(`item`, {
        url: `/:itemId`,
        component: `item`,
        resolve: {
          item: ($stateParams, ItemService) => ItemService.getById(+$stateParams.itemId)
        }
      });
  });

I'm considering allowing any type of component binding, but I'm struggling with a compelling or meaningful use case for "&" binding from a resolve. Would the callback be defined on a parent state, or the same state? Would it interact with other resolves? Respond to UI events?

From my point of view, the callback should work just like any other resolve, the only "unusual" thing here is that the resolve returns a function. Interaction with other resolves would be possible through the closure.

I'm wondering if the intention is to get some "smart component" functionality? Would it be simpler to introduce a "smart component" that wraps your other component, and route to that instead?

Wrapping the component within a "smart component" is definitely an option, thanks for pointing me to it.

If I'll find any other meaningful and compelling use case for the & binding, I'll let you know for sure :)

@christopherthielen
Copy link
Contributor Author

@fpipita After playing with & bindings a bit, I don't think I want to support it for route-to-component.

Here is my reasoning...

  • & bindings are for executing a callback in the context of the parent scope.
  • They allow the user to define parameters, and pass parameter values from the component to the callback. The params are injected into the callback function.
  • To determine what the callback wants injected, we would have to annotate the callback function
  • The callback function isn't when the template provider is building the route-to-component template
  • The parent scope context for a route-to-component isn't useful
  • Your use case could be implemented by using a < binding instead, and return a function from the matching resolve

@samithaf
Copy link

samithaf commented May 25, 2016

Correct me if I am wrong option 4 syntax needed to updated as follows.

bindings: { input1: "=foo", input2: "=bar" }
or
bindings: { input1: "<foo", input2: "<bar" }

@siddharth-pandey
Copy link

Do we have any working examples of using ui-router and components with all or any options discussed above? Based on my search, I just get this issue and examples of using template: '<c></c>' within a state in the search results.

@fasfsfgs
Copy link

fasfsfgs commented Jul 23, 2016

@siddharth-pandey I'm successfully using this config (with version 1.0.0-beta.1):

    $stateProvider.state('example', {
      url: '/example',
      component: 'example',
      resolve: {
        param1: param1,
        param2: param2
      }
    });

    angular.module('app').component('example', {
      bindings: {
        param1: '<',
        param2: '<'
      }
    });

@siddharth-pandey
Copy link

@fasfsfgs Thanks for sharing your code. I'm using ui-router's version v0.2.18 and have tried to render similar code. Is this way of specifying component name supported in v0.2.18? If not, which one should I use with Angular version 1.5 to use components?

@fasfsfgs
Copy link

@siddharth-pandey As far as I know only ui router 1.0 (which is not released yet) deals directly with components. So you'd have to use a beta version to get this functionality (like I have). If you stick with released versions, your solution with templates is the one to go.

@Luddinus
Copy link

Luddinus commented Sep 7, 2016

Not working resolve and component (method 2) using 1.0 beta1...

The resolved parameters are undefined in the controller component

edit: Does not work with named ui-views

fiddle: http://jsfiddle.net/789Ks/96/

@archasek
Copy link

@Luddinus did you find a workaround for that? Same problem here..

@Luddinus
Copy link

Luddinus commented Sep 15, 2016

@archasek Yes, here is the link: #2976

Basically that I had "resolve" property into "views"

@bokswagen
Copy link

on v0.2.18, you can use option1: with template. works fine for now if you don't want to upgrade to the beta.

@christopherthielen
Copy link
Contributor Author

Also note that there is a polyfill for 0.2.18 based on my original route-to-component plunker: https://github.com/softsimon/ui-router-route-to-components

@christopherthielen christopherthielen added this to the 1.0.0-alpha.1 milestone Jan 12, 2017
turtle220 pushed a commit to turtle220/laravel5-angular-materialUI that referenced this issue Jul 17, 2020
The must have update dependencie is the `angular-ui-router` simply because it now support the components as template as mention here angular-ui/ui-router#2627 the other ones, just to have everything updated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests