atmd atmd - 1 month ago 22
Scala Question

variables in play views

I've found a few answers online to my issue but all for older versions of play which no longer work.

I am trying to find a way to use a variable as part of the template path.
I understand that this will remove the type checking etc, but its for a generator/prototyping/internal tool and isn't something that will see the public/outside world.

here's the codez:

The best answer I can find as a work around is:

def index(page: String) = Action {
page match {
case "something" => Ok(views.html.examples.something())
case "else" => Ok(views.html.examples.else())
case "another" => Ok(views.html.examples.elements.another())
}
}


ideally something like this would make more sense:

def index(page: String) = Action {
Ok(views.html.getClassFromStringOrSomething(page));
}


I am using the latest play framework (2.5.*) with scala.

is using a match the only way?
There must be a dynamic why to return a view?

n.b. I have found loads of suggestions online which use
play.api.templates.Html/play.twirl.api.Html
which seem to no longer work,
Html
can't be found so im guessing the api has changes other the versions.

Using that I get:

def index(page: String, parent: String) = Action {

val clazz: Class[_] = Play.current.classloader.loadClass(page)
val render: Method = clazz.getDeclaredMethod("render")
val view = Some(render.invoke(clazz).asInstanceOf[play.twirl.api.Html])
Ok(view)
}


This give me the error
Cannot write an instance of Some[play.twirl.api.Html] to HTTP response. Try to define a Writeable[Some[play.twirl.api.Html]]
and this is where I'm stuck

Answer

I've added an answer here solely to help anyone else hitting this issue.

@Mikesname was correct, remove the Some() and it 'should' all work fine.

The issue was that I was dynamically loading the class/view name, but I wasn't using the full class name. I was loading something when I should have been loading view.html.something.

Heres the code I used to get the feature working (needs cleaning up but it's working, also accounts for views named index inside a directory, i.e. views.html.parent -> /parent/index.scala.html

    package controllers

import javax.inject.{Inject, Singleton}

import play.api.Play
import play.api.mvc.{Action, Controller}

import play.twirl.api.Html

@Singleton
class PathController @Inject() extends Controller {

  def index(page: String, parent: String, grandparent: String) = Action {
     Ok(buildView(buildClassName(page, parent, grandparent)))
  }

  def buildClassName(page: String, parent: String, grandparent: String): String = {
    val pathGrandParent = if (grandparent != null) grandparent  + '.' else ""
    val pathParent = if (parent != null) parent + '.' else ""

    "views.html." + pathGrandParent + pathParent + page
  }

  def loadClass(name: String): Class[_] =  {
    try {
      Play.current.classloader.loadClass(name)
    } catch {
        case _: Throwable => {
          Play.current.classloader.loadClass(name + ".index")
        }
    }
  }

  def buildView(name: String): Html = {
    val clazz = loadClass(name)
    clazz.getDeclaredMethod("render").invoke(clazz).asInstanceOf[play.twirl.api.Html]
  }
}

I'd imagine that there is a much, much better way of doing the index view part then a try/catch, but it's fine for the tool I'm building

Comments