Skip to content
This repository has been archived by the owner on Sep 15, 2024. It is now read-only.

UIRouter: example how to set controller of a State? #45

Open
mackler opened this issue Jul 7, 2015 · 2 comments
Open

UIRouter: example how to set controller of a State? #45

mackler opened this issue Jul 7, 2015 · 2 comments

Comments

@mackler
Copy link

mackler commented Jul 7, 2015

I'm new to this library, so hopefully this is a very easy question. I am using scalajs-angular version 0.5-SNAPSHOT and angular-ui-router version 0.2.15.

I am trying to implement the first example on the AngularUI Router tutorial, which is given in JavaScript. I have it working using scalajs-angular except for one part: the setting of the controllers on the state objects.

Update and Tentative Conclusion

I have, if not solved this, at least reached a work-around, posted in detail at the end. In brief, near as I can tell controllers must be registered in order for them to have scope injected, which is not possible with an anonymous function as in the JavaScript version. I could be totally wrong about this, and I invite correction.

End of Update. This Is My Original Questions:

In the JavaScript tutorial it looks like this:

    .state('state1.list', {
          url: "/list",
          templateUrl: "partials/state1.list.html",
          controller: function($scope) {
            $scope.items = ["A", "List", "Of", "Items"];
          }
        })

Looking at the companion object for State I see the factory apply() method has parameters for url and templateUrl, both of type String, and I set those no problem. I have failed, however, to set the controller member, which the State trait defines as type js.Any. Since in the JavaScript version, the value of controller is a function that takes a scope object and sets that object's items property, I tried setting controller to be a Scala function object:

      state("state1.list", {
        val s = State(
          url = "/list",
          templateUrl = "partials/state1_list.html"
        )
        s.controller = (scope: State1Scope) => scope.items = js.Array("A", "List", "Of", "Items")
        s
      })

With State1Scope defined like this:

    trait State1Scope extends Scope {
      var items: js.Array[String] = js.native
    }

I also tried it with the type of State1Scope.items being a Scala Array rather than js.Array.

I also attempted to set the value of controller to be a JavaScript function, like this:

        s.controller = new scalajs.js.Function1[State1Scope, Unit] {
          override def apply(scope: State1Scope) { scope.items = js.Array[String]("A", "List", "Of", "Items") }
        }

Not only did that fail, but I also got a warning that Members of traits, classes and objects extending js.Any may only contain members that call js.native.

The error I am getting is in my browser console:

    Error: [$injector:unpr] Unknown provider: scope$2Provider <- scope$2
    http://errors.angularjs.org/1.4.1/$injector/unpr?p0=scope%242Provider%20%3C-<div ui-view="" class="ng-scope">cope%242

I have annotated my Config object with @injectable("StateConfig").

Other Things I Have Tried

Here are some other things I have tried, all unsuccessful:

I thought maybe of setting controller to be a Controller object like this:

        s.controller = new Service with Controller[State1Scope] {
          override def initialize() {
            scope.items = js.Array[String]("A", "List", "Of", "Items")
          }
        }.asInstanceOf[js.Object]

But that won't work because scope is never defined. (In the JavaScript version, the scope is passed as an argument to the function that is the value of the controller property. The analogous way in Scala would seem to be to make scope be an argument to the apply() method of a function object.)

Which led me to try this:

         s.controller = new Service with Controller[State1Scope] {
          private var scopeOption: Option[State1Scope] = None
          override def scope = scopeOption match {
            case None => throw new NoSuchElementException("scope unset")
            case Some(sp) => sp
          }
          def apply(passedScope: State1Scope) { scopeOption = Some(passedScope) }
          overide def initialize() {
            scope.items = js.Array[String]("A", "List", "Of", "Items")
          }
         }.asInstanceOf[js.Object]

That gave me a different error in the browser console: Error: [ng:areq] Argument 'fn' is not a function, got Object which sounds as if I'm going in the wrong direction.

I also have tried first defining the controller as a separate object. I copied the example Controller from from the scalajs-angular documentation under the heading Property Based Dependency Injection, changing its type annotation of Scope to my Scope sub-trait, but that failed to compile with the error:

    [error] ... method scope_= overrides nothing
    [error]     override var scope: State1Scope = _
    [error]                  ^

I hope I'm missing something really basic. I'll be happy to post more of my code if that will help. I think if I could see a working example that would be enough to get me unstuck. I looked at olivergg's scalajs-ionic-starttabs app but there are no instances of setting the controller of a State in the relevant file. I also looked for any tests in the scalajs-angular repository that might set the controller member of a State, but I failed to find any.

My sincere thanks and appreciation to anyone who can give me any guidance on this.

End of Original Question

Ultimately I gave up on trying to create the controllers as function literals within invocations of StateProvider.state(). Rather I defined them like this:

    @injectable("State1Ctrl")
    class State1Controller(scope: State1Scope) extends AbstractController[State1Scope](scope) {
      scope.items = js.Array("A", "List", "Of", "Items")
    }

and also registered them in my app module:

    module.controller[State1Controller]
    module.controller[State2Controller]

and then my invocation of StateProvider.state in my StateConfig looks like:

      state("state1.list", State(
        url = "/list",
        views = Map("viewA" -> View("partials/state1_list.html", "State1Ctrl"))
      )).

The two arguments to the application of the View object are, respectively, the former value of the urlTemplate argument, and the name of the controller given in the @injectable annotation.

Finally, I had to update the corresponding ui-view attributes, setting their values to the keys of the Map value of the views argument to StateProvider.state():

    <div ui-view="viewB"></div>

So this Scala version does what the JavaScript example tutorial does, though it's built a bit differently with named controllers rather than anonymous functions. I am a long way from having a solid understanding of everything going on here, so if anyone feels like shedding light on this situation, I will be eager to learn all I can.

@mysticfall
Copy link
Member

Wow, that was a very detailed analysis, and thanks much for taking time to brought it to my attention.

The cause of the problem is that Scala's controller instance itself is not a valid Javascript controller function, so we can't substitute the one with the other, as you already found out. It's just one of the unfortunate limitation of wrapping Angular API with Scala.js.

To workaround the problem, scalajs-angular creates a proxy javascript function, using Angular's array based DI mechanism, when it processes a Scala controller instance (you can click here to see how it works).

It works good for regular named controllers, but it can be problematic for anonymous ones, like used in your example.

I still haven't found a perfect solution for this problem, but I managed to workaround for the similar problem with Directives, by providing a method to convert your Scala controller to the matching Javascript proxy:

(In Directive.scala)

protected def proxy[A <: Controller[ScopeType]]: js.Any = macro ServiceProxy.newClassWrapper[A]

protected def proxy[A <: Controller[ScopeType]](target: A): js.Any = macro ServiceProxy.newObjectWrapper[A]

I think a similar approach could be used with the case you mentioned, so I just moved those methods to Controller so they can be used outside the context of directives.

Please try the latest snapshot and see if it solves the problem. Thanks!

@mackler
Copy link
Author

mackler commented Jul 12, 2015

Thank you Xavier for the response, the explanation and for making the change to Controller.scala. I'm afraid to say my current level of understanding is inadequate for the task of applying your solution. As I understand, you're saying the Controller.proxy() method will turn a Scala Controller into a JavaScript controller function, but I am missing how to apply it. My best guess would be:

  state("state1.list", {
    val s = State(
      url = "/list",
      templateUrl = "partials/state1_list.html"
    )
    s.controller = Controller.proxy(new Controller[SomeScope] {
      @inject
      override var scope: Scope = _
      override def initialize() {
        scope.items = js.Array("A", "List", "Of", "Items")
      }
    })
    s
  })

But that would require the named scope trait SomeScope which is inconsistent with the idea of setting the State.controller to an anonymous controller value. And even if that were correct, it's so many lines of code compared to a standard Scala function object

    s.controller = (scope: State1Scope) => scope.items = js.Array("A", "List", "Of", "Items")

it hardly provides any advantage over defining a named controller, in terms of concision and the number of lines-of-code required.

In any event, I have since figured out that the State object's apply method will take a controller, but as a member of a View, as in:

  state("state1.list", State(
    url = "/list",
    views = Map("named-view" -> View("partials/state1_list.html", "someCtrl"))
  )).

That much eliminated several extra lines of code and avoids mutation of a State instance. And beyond that, my actual application can use named controllers, so my question of how to implement the tutorial example is really more a case of academic curiosity than any practical requirement. Your information that it's just a limitation of wrapping Angular API with Scala.js satisfies my curiosity, and if you are so generous with your time to answer my questions, I have others more pressing.

I'm happy to close this issue now. It may be a while before I learn enough about this library, plus Scala.js, plus AngularUI Routing to be able to apply successfully the solution you have proposed. Again my thanks for your very responsive reply.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants