From 2f6b82915f2b953e0497cdea754475a90a59353d Mon Sep 17 00:00:00 2001 From: Sanjana Date: Wed, 5 Aug 2026 04:13:37 +0530 Subject: [PATCH 1/3] Add Grails 8 creating-your-first-grails-app guide. --- conf/guides.yml | 45 ++++++- .../v8/guide/controllers.adoc | 39 ++++++ .../v8/guide/creatingTheApp.adoc | 14 ++ .../v8/guide/domainClasses.adoc | 27 ++++ .../v8/guide/gettingStarted.adoc | 14 ++ .../v8/guide/helpWithGrails.adoc | 1 + .../v8/guide/introduction.adoc | 3 + .../v8/guide/requirements.adoc | 5 + .../v8/guide/runningTheApp.adoc | 10 ++ .../v8/guide/services.adoc | 37 ++++++ .../v8/guide/summary.adoc | 9 ++ .../v8/guide/testing.adoc | 27 ++++ .../v8/guide/views.adoc | 25 ++++ .../controllers/example/HomeController.groovy | 21 +++ .../controllers/example/MakeController.groovy | 7 + .../example/ModelController.groovy | 7 + .../controllers/example/UrlMappings.groovy | 17 +++ .../example/VehicleController.groovy | 122 ++++++++++++++++++ .../grails-app/domain/example/Make.groovy | 14 ++ .../grails-app/domain/example/Model.groovy | 16 +++ .../grails-app/domain/example/Vehicle.groovy | 18 +++ .../grails-app/init/example/BootStrap.groovy | 27 ++++ .../services/example/MakeService.groovy | 8 ++ .../services/example/ModelService.groovy | 8 ++ .../example/ValueEstimateService.groovy | 12 ++ .../services/example/VehicleService.groovy | 8 ++ .../snippets/grails-app/views/home/index.gsp | 36 ++++++ .../grails-app/views/vehicle/create.gsp | 43 ++++++ .../grails-app/views/vehicle/edit.gsp | 45 +++++++ .../grails-app/views/vehicle/index.gsp | 29 +++++ .../grails-app/views/vehicle/show.gsp | 41 ++++++ .../groovy/example/HomePageSpec.groovy | 18 +++ .../example/ValueEstimateServiceSpec.groovy | 27 ++++ 33 files changed, 779 insertions(+), 1 deletion(-) create mode 100644 guides/creating-your-first-grails-app/v8/guide/controllers.adoc create mode 100644 guides/creating-your-first-grails-app/v8/guide/creatingTheApp.adoc create mode 100644 guides/creating-your-first-grails-app/v8/guide/domainClasses.adoc create mode 100644 guides/creating-your-first-grails-app/v8/guide/gettingStarted.adoc create mode 100644 guides/creating-your-first-grails-app/v8/guide/helpWithGrails.adoc create mode 100644 guides/creating-your-first-grails-app/v8/guide/introduction.adoc create mode 100644 guides/creating-your-first-grails-app/v8/guide/requirements.adoc create mode 100644 guides/creating-your-first-grails-app/v8/guide/runningTheApp.adoc create mode 100644 guides/creating-your-first-grails-app/v8/guide/services.adoc create mode 100644 guides/creating-your-first-grails-app/v8/guide/summary.adoc create mode 100644 guides/creating-your-first-grails-app/v8/guide/testing.adoc create mode 100644 guides/creating-your-first-grails-app/v8/guide/views.adoc create mode 100644 guides/creating-your-first-grails-app/v8/snippets/grails-app/controllers/example/HomeController.groovy create mode 100644 guides/creating-your-first-grails-app/v8/snippets/grails-app/controllers/example/MakeController.groovy create mode 100644 guides/creating-your-first-grails-app/v8/snippets/grails-app/controllers/example/ModelController.groovy create mode 100644 guides/creating-your-first-grails-app/v8/snippets/grails-app/controllers/example/UrlMappings.groovy create mode 100644 guides/creating-your-first-grails-app/v8/snippets/grails-app/controllers/example/VehicleController.groovy create mode 100644 guides/creating-your-first-grails-app/v8/snippets/grails-app/domain/example/Make.groovy create mode 100644 guides/creating-your-first-grails-app/v8/snippets/grails-app/domain/example/Model.groovy create mode 100644 guides/creating-your-first-grails-app/v8/snippets/grails-app/domain/example/Vehicle.groovy create mode 100644 guides/creating-your-first-grails-app/v8/snippets/grails-app/init/example/BootStrap.groovy create mode 100644 guides/creating-your-first-grails-app/v8/snippets/grails-app/services/example/MakeService.groovy create mode 100644 guides/creating-your-first-grails-app/v8/snippets/grails-app/services/example/ModelService.groovy create mode 100644 guides/creating-your-first-grails-app/v8/snippets/grails-app/services/example/ValueEstimateService.groovy create mode 100644 guides/creating-your-first-grails-app/v8/snippets/grails-app/services/example/VehicleService.groovy create mode 100644 guides/creating-your-first-grails-app/v8/snippets/grails-app/views/home/index.gsp create mode 100644 guides/creating-your-first-grails-app/v8/snippets/grails-app/views/vehicle/create.gsp create mode 100644 guides/creating-your-first-grails-app/v8/snippets/grails-app/views/vehicle/edit.gsp create mode 100644 guides/creating-your-first-grails-app/v8/snippets/grails-app/views/vehicle/index.gsp create mode 100644 guides/creating-your-first-grails-app/v8/snippets/grails-app/views/vehicle/show.gsp create mode 100644 guides/creating-your-first-grails-app/v8/snippets/src/integration-test/groovy/example/HomePageSpec.groovy create mode 100644 guides/creating-your-first-grails-app/v8/snippets/src/test/groovy/example/ValueEstimateServiceSpec.groovy diff --git a/conf/guides.yml b/conf/guides.yml index 94d905d3d51..24c3921defe 100644 --- a/conf/guides.yml +++ b/conf/guides.yml @@ -478,6 +478,7 @@ guides: subtitle: 'Learn how to create your first Grails app' authors: - 'Zachary Klein' + - 'Sanjana' category: 'Grails Apprentice' publicationDate: '2017-01-23' versions: @@ -687,7 +688,49 @@ guides: deployment: title: Deploying your App helpWithGrails: - title: Do you need help with Grails? + title: Do you need help with Grails? + '8': + sourcePath: guides/creating-your-first-grails-app/v8 + publicationDate: '2026-08-05' + tags: + - 'beginner' + - 'domain-classes' + - 'controllers' + - 'services' + - 'gsp' + - 'unit-tests' + - 'integration-tests' + - 'scaffolding' + - 'getting-started' + - 'crud' + sampleRef: + repo: 'grails-guides/creating-your-first-grails-app' + branch: 'grails8' + toc: + introduction: + title: Introduction + requirements: + title: What you will need + gettingStarted: + title: Getting Started + creatingTheApp: + title: Creating the app + runningTheApp: + title: Running the app + domainClasses: + title: Domain classes + controllers: + title: Controllers and scaffolding + views: + title: GSP views + services: + title: Services + testing: + title: Testing + summary: + title: Summary + helpWithGrails: + title: Do you need help with Grails? - name: 'database-per-tenant' title: 'Database per Tenant Multi-Tenancy' diff --git a/guides/creating-your-first-grails-app/v8/guide/controllers.adoc b/guides/creating-your-first-grails-app/v8/guide/controllers.adoc new file mode 100644 index 00000000000..0548de79341 --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/guide/controllers.adoc @@ -0,0 +1,39 @@ +Map the root URL to a home controller and use scaffolding for Make / Model CRUD. + +Point `/` at `HomeController`: + +[source,groovy] +.grails-app/controllers/example/UrlMappings.groovy +---- +include::../snippets/grails-app/controllers/example/UrlMappings.groovy[] +---- + +`HomeController` lists vehicles and lets the visitor update a session name: + +[source,groovy] +.grails-app/controllers/example/HomeController.groovy +---- +include::../snippets/grails-app/controllers/example/HomeController.groovy[] +---- + +For `Make` and `Model`, Grails 8 scaffolding is annotation-based: + +[source,groovy] +.grails-app/controllers/example/MakeController.groovy +---- +include::../snippets/grails-app/controllers/example/MakeController.groovy[] +---- + +[source,groovy] +.grails-app/controllers/example/ModelController.groovy +---- +include::../snippets/grails-app/controllers/example/ModelController.groovy[] +---- + +`VehicleController` is a hand-written CRUD controller. The `show` action adds an estimated value from `ValueEstimateService`: + +[source,groovy] +.grails-app/controllers/example/VehicleController.groovy +---- +include::../snippets/grails-app/controllers/example/VehicleController.groovy[] +---- diff --git a/guides/creating-your-first-grails-app/v8/guide/creatingTheApp.adoc b/guides/creating-your-first-grails-app/v8/guide/creatingTheApp.adoc new file mode 100644 index 00000000000..dddde44944d --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/guide/creatingTheApp.adoc @@ -0,0 +1,14 @@ +You can recreate `initial/` yourself with the Grails Application Forge: + +[source,bash] +---- +curl -O https://start.grails.org/creating-your-first-grails-app.zip \ + -d version=8.0.0-SNAPSHOT \ + -d profile=web \ + -d features=gsp,hibernate5,scaffolding,geb,h2 +unzip creating-your-first-grails-app.zip +---- + +Or use the https://start.grails.org[web UI], choose Grails 8, the `web` profile, and the features you need, then download the zip. + +This guide uses the cloned `initial/` / `complete/` trees so paths and package names (`example`) stay consistent with the sample. diff --git a/guides/creating-your-first-grails-app/v8/guide/domainClasses.adoc b/guides/creating-your-first-grails-app/v8/guide/domainClasses.adoc new file mode 100644 index 00000000000..bbd75e86f88 --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/guide/domainClasses.adoc @@ -0,0 +1,27 @@ +Model a simple vehicle inventory with three domain classes under `grails-app/domain/example/`. + +`Make` is a plain name: + +[source,groovy] +.grails-app/domain/example/Make.groovy +---- +include::../snippets/grails-app/domain/example/Make.groovy[] +---- + +`Model` belongs to a `Make`: + +[source,groovy] +.grails-app/domain/example/Model.groovy +---- +include::../snippets/grails-app/domain/example/Model.groovy[] +---- + +`Vehicle` ties name, year, make, and model together: + +[source,groovy] +.grails-app/domain/example/Vehicle.groovy +---- +include::../snippets/grails-app/domain/example/Vehicle.groovy[] +---- + +GORM maps these classes to tables automatically when you run the app against H2 (the starter default). diff --git a/guides/creating-your-first-grails-app/v8/guide/gettingStarted.adoc b/guides/creating-your-first-grails-app/v8/guide/gettingStarted.adoc new file mode 100644 index 00000000000..21a2d22bae6 --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/guide/gettingStarted.adoc @@ -0,0 +1,14 @@ +Clone the repository and verify the starter project: + +[source,bash] +---- +git clone -b grails8 https://github.com/grails-guides/creating-your-first-grails-app.git +cd creating-your-first-grails-app/initial +./gradlew test +---- + +The `initial/` project is a Grails 8 web starter from https://start.grails.org[start.grails.org] (`web` profile, GSP, H2, scaffolding, Spock, Geb). It has no domain model yet. + +To skip ahead, `cd ../complete` and run the same commands; that tree contains the finished Make / Model / Vehicle sample. + +Both `initial` and `complete` must pass in CI (see `.github/workflows/grails8.yml`). diff --git a/guides/creating-your-first-grails-app/v8/guide/helpWithGrails.adoc b/guides/creating-your-first-grails-app/v8/guide/helpWithGrails.adoc new file mode 100644 index 00000000000..e062f614b1a --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/guide/helpWithGrails.adoc @@ -0,0 +1 @@ +include::{commondir}/common-helpWithGrails.adoc[] diff --git a/guides/creating-your-first-grails-app/v8/guide/introduction.adoc b/guides/creating-your-first-grails-app/v8/guide/introduction.adoc new file mode 100644 index 00000000000..535b77431a7 --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/guide/introduction.adoc @@ -0,0 +1,3 @@ +Build a first Apache Grails 8 web application: domain classes, controllers, GSP views, GORM data services, scaffolding, and Spock tests. + +This guide follows the standard Grails guides layout: work in the `initial/` project and compare your progress with `complete/`. diff --git a/guides/creating-your-first-grails-app/v8/guide/requirements.adoc b/guides/creating-your-first-grails-app/v8/guide/requirements.adoc new file mode 100644 index 00000000000..21ac3ef4b06 --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/guide/requirements.adoc @@ -0,0 +1,5 @@ +* Approximately 60 minutes +* JDK 21 (Apache Grails 8 requires Java 21) +* A text editor or IDE +* The Gradle wrapper bundled in `initial/` and `complete/` (no separate Grails SDK install required) +* Docker, if you want to run Geb integration tests (`./gradlew integrationTest`) diff --git a/guides/creating-your-first-grails-app/v8/guide/runningTheApp.adoc b/guides/creating-your-first-grails-app/v8/guide/runningTheApp.adoc new file mode 100644 index 00000000000..909299c8609 --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/guide/runningTheApp.adoc @@ -0,0 +1,10 @@ +From `complete/` (or your working copy of `initial/` once you have added the domain model): + +[source,bash] +---- +./gradlew bootRun +---- + +Open http://localhost:8080/[http://localhost:8080/]. You should see the home page with seeded vehicles. Try `/vehicle/index`, `/make/index`, and `/model/index` as well. + +The default server port is `8080`. To change it, set `server.port` in `grails-app/conf/application.yml` or pass `-Dserver.port=8090` to Gradle. diff --git a/guides/creating-your-first-grails-app/v8/guide/services.adoc b/guides/creating-your-first-grails-app/v8/guide/services.adoc new file mode 100644 index 00000000000..15d3d5214bb --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/guide/services.adoc @@ -0,0 +1,37 @@ +Prefer GORM data services for persistence and a small transactional service for domain logic. + +Data services seed `BootStrap` without writing repository boilerplate: + +[source,groovy] +.grails-app/services/example/MakeService.groovy +---- +include::../snippets/grails-app/services/example/MakeService.groovy[] +---- + +[source,groovy] +.grails-app/services/example/ModelService.groovy +---- +include::../snippets/grails-app/services/example/ModelService.groovy[] +---- + +[source,groovy] +.grails-app/services/example/VehicleService.groovy +---- +include::../snippets/grails-app/services/example/VehicleService.groovy[] +---- + +`BootStrap` uses those services to insert sample rows on startup: + +[source,groovy] +.grails-app/init/example/BootStrap.groovy +---- +include::../snippets/grails-app/init/example/BootStrap.groovy[] +---- + +`ValueEstimateService` is a regular Grails service used by `VehicleController.show`: + +[source,groovy] +.grails-app/services/example/ValueEstimateService.groovy +---- +include::../snippets/grails-app/services/example/ValueEstimateService.groovy[] +---- diff --git a/guides/creating-your-first-grails-app/v8/guide/summary.adoc b/guides/creating-your-first-grails-app/v8/guide/summary.adoc new file mode 100644 index 00000000000..37753952bb1 --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/guide/summary.adoc @@ -0,0 +1,9 @@ +You built a first Grails 8 web app: + +* Domain classes for `Make`, `Model`, and `Vehicle` +* Annotation scaffolding for Make / Model and a custom `VehicleController` +* GSP views with Fields tags and a home page that lists seeded vehicles +* GORM data services plus `ValueEstimateService` +* Spock unit tests and a Geb integration spec + +Next steps: add validation messages, secure the CRUD actions, or swap H2 for PostgreSQL in production. diff --git a/guides/creating-your-first-grails-app/v8/guide/testing.adoc b/guides/creating-your-first-grails-app/v8/guide/testing.adoc new file mode 100644 index 00000000000..fe79c6236f9 --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/guide/testing.adoc @@ -0,0 +1,27 @@ +Unit-test the estimate service with Spock and the Grails testing mixins: + +[source,groovy] +.src/test/groovy/example/ValueEstimateServiceSpec.groovy +---- +include::../snippets/src/test/groovy/example/ValueEstimateServiceSpec.groovy[] +---- + +Run unit tests: + +[source,bash] +---- +./gradlew test +---- + +Geb integration tests use Testcontainers (`ContainerGebSpec`). Docker must be running: + +[source,groovy] +.src/integration-test/groovy/example/HomePageSpec.groovy +---- +include::../snippets/src/integration-test/groovy/example/HomePageSpec.groovy[] +---- + +[source,bash] +---- +./gradlew integrationTest +---- diff --git a/guides/creating-your-first-grails-app/v8/guide/views.adoc b/guides/creating-your-first-grails-app/v8/guide/views.adoc new file mode 100644 index 00000000000..aedb03da558 --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/guide/views.adoc @@ -0,0 +1,25 @@ +GSPs under `grails-app/views/` render the HTML. The home page uses `` and ``: + +[source,gsp] +.grails-app/views/home/index.gsp +---- +include::../snippets/grails-app/views/home/index.gsp[] +---- + +Vehicle list and form pages use the Fields plugin tags (``, ``, ``). The list view: + +[source,gsp] +.grails-app/views/vehicle/index.gsp +---- +include::../snippets/grails-app/views/vehicle/index.gsp[] +---- + +The show page displays the estimated value next to the vehicle: + +[source,gsp] +.grails-app/views/vehicle/show.gsp +---- +include::../snippets/grails-app/views/vehicle/show.gsp[] +---- + +Create and edit forms live in `create.gsp` and `edit.gsp` and call ``. diff --git a/guides/creating-your-first-grails-app/v8/snippets/grails-app/controllers/example/HomeController.groovy b/guides/creating-your-first-grails-app/v8/snippets/grails-app/controllers/example/HomeController.groovy new file mode 100644 index 00000000000..40601ef7823 --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/snippets/grails-app/controllers/example/HomeController.groovy @@ -0,0 +1,21 @@ +package example + +import grails.gorm.transactions.ReadOnly + +class HomeController { + + @ReadOnly + def index() { + respond([ + name: session.name ?: 'User', + vehicleList: Vehicle.list(), + vehicleTotal: Vehicle.count() + ]) + } + + def updateName(String name) { + session.name = name + flash.message = 'Name has been updated' + redirect action: 'index' + } +} diff --git a/guides/creating-your-first-grails-app/v8/snippets/grails-app/controllers/example/MakeController.groovy b/guides/creating-your-first-grails-app/v8/snippets/grails-app/controllers/example/MakeController.groovy new file mode 100644 index 00000000000..7963dc8529e --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/snippets/grails-app/controllers/example/MakeController.groovy @@ -0,0 +1,7 @@ +package example + +import grails.plugin.scaffolding.annotation.Scaffold + +@Scaffold(Make) +class MakeController { +} diff --git a/guides/creating-your-first-grails-app/v8/snippets/grails-app/controllers/example/ModelController.groovy b/guides/creating-your-first-grails-app/v8/snippets/grails-app/controllers/example/ModelController.groovy new file mode 100644 index 00000000000..8b113b1ea4a --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/snippets/grails-app/controllers/example/ModelController.groovy @@ -0,0 +1,7 @@ +package example + +import grails.plugin.scaffolding.annotation.Scaffold + +@Scaffold(Model) +class ModelController { +} diff --git a/guides/creating-your-first-grails-app/v8/snippets/grails-app/controllers/example/UrlMappings.groovy b/guides/creating-your-first-grails-app/v8/snippets/grails-app/controllers/example/UrlMappings.groovy new file mode 100644 index 00000000000..473a70389cc --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/snippets/grails-app/controllers/example/UrlMappings.groovy @@ -0,0 +1,17 @@ +package example + +class UrlMappings { + + static mappings = { + "/$namespace/$controller/$action?/$id?(.$format)?" {} + "/$controller/$action?/$id?(.$format)?" { + constraints { + // apply constraints here + } + } + + '/'(controller: 'home') + '500'(view: '/error') + '404'(view: '/notFound') + } +} diff --git a/guides/creating-your-first-grails-app/v8/snippets/grails-app/controllers/example/VehicleController.groovy b/guides/creating-your-first-grails-app/v8/snippets/grails-app/controllers/example/VehicleController.groovy new file mode 100644 index 00000000000..8869d760e3b --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/snippets/grails-app/controllers/example/VehicleController.groovy @@ -0,0 +1,122 @@ +package example + +import static org.springframework.http.HttpStatus.CREATED +import static org.springframework.http.HttpStatus.NOT_FOUND +import static org.springframework.http.HttpStatus.NO_CONTENT +import static org.springframework.http.HttpStatus.OK + +import grails.gorm.transactions.ReadOnly +import grails.gorm.transactions.Transactional + +@ReadOnly +class VehicleController { + + static allowedMethods = [save: 'POST', update: 'PUT', delete: 'DELETE'] + + ValueEstimateService valueEstimateService + + def index(Integer max) { + params.max = Math.min(max ?: 10, 100) + respond Vehicle.list(params), model: [vehicleCount: Vehicle.count()] + } + + def show(Vehicle vehicle) { + respond vehicle, model: [estimatedValue: valueEstimateService.getEstimate(vehicle)] + } + + def create() { + respond new Vehicle(params) + } + + @Transactional + def save(Vehicle vehicle) { + if (vehicle == null) { + notFound() + return + } + + if (vehicle.hasErrors()) { + transactionStatus.setRollbackOnly() + respond vehicle.errors, view: 'create' + return + } + + vehicle.save flush: true + + request.withFormat { + form multipartForm { + flash.message = message( + code: 'default.created.message', + args: [message(code: 'vehicle.label', default: 'Vehicle'), vehicle.id] + ) + redirect vehicle + } + '*' { respond vehicle, [status: CREATED] } + } + } + + def edit(Vehicle vehicle) { + respond vehicle + } + + @Transactional + def update(Vehicle vehicle) { + if (vehicle == null) { + notFound() + return + } + + if (vehicle.hasErrors()) { + transactionStatus.setRollbackOnly() + respond vehicle.errors, view: 'edit' + return + } + + vehicle.save flush: true + + request.withFormat { + form multipartForm { + flash.message = message( + code: 'default.updated.message', + args: [message(code: 'vehicle.label', default: 'Vehicle'), vehicle.id] + ) + redirect vehicle + } + '*' { respond vehicle, [status: OK] } + } + } + + @Transactional + def delete(Vehicle vehicle) { + if (vehicle == null) { + notFound() + return + } + + vehicle.delete flush: true + + request.withFormat { + form multipartForm { + flash.message = message( + code: 'default.deleted.message', + args: [message(code: 'vehicle.label', default: 'Vehicle'), vehicle.id] + ) + redirect action: 'index', method: 'GET' + } + '*' { render status: NO_CONTENT } + } + } + + protected void notFound() { + request.withFormat { + form multipartForm { + flash.message = message( + code: 'default.not.found.message', + args: [message(code: 'vehicle.label', default: 'Vehicle'), params.id] + ) + redirect action: 'index', method: 'GET' + } + '*' { render status: NOT_FOUND } + } + } +} diff --git a/guides/creating-your-first-grails-app/v8/snippets/grails-app/domain/example/Make.groovy b/guides/creating-your-first-grails-app/v8/snippets/grails-app/domain/example/Make.groovy new file mode 100644 index 00000000000..e5e55fcc8c8 --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/snippets/grails-app/domain/example/Make.groovy @@ -0,0 +1,14 @@ +package example + +class Make { + + String name + + static constraints = { + name blank: false, maxSize: 255 + } + + String toString() { + name + } +} diff --git a/guides/creating-your-first-grails-app/v8/snippets/grails-app/domain/example/Model.groovy b/guides/creating-your-first-grails-app/v8/snippets/grails-app/domain/example/Model.groovy new file mode 100644 index 00000000000..864e6efaa7c --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/snippets/grails-app/domain/example/Model.groovy @@ -0,0 +1,16 @@ +package example + +class Model { + + String name + + static belongsTo = [make: Make] + + static constraints = { + name blank: false, maxSize: 255 + } + + String toString() { + name + } +} diff --git a/guides/creating-your-first-grails-app/v8/snippets/grails-app/domain/example/Vehicle.groovy b/guides/creating-your-first-grails-app/v8/snippets/grails-app/domain/example/Vehicle.groovy new file mode 100644 index 00000000000..4f7e1cbba29 --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/snippets/grails-app/domain/example/Vehicle.groovy @@ -0,0 +1,18 @@ +package example + +class Vehicle { + + String name + Integer year + Make make + Model model + + static constraints = { + name blank: false, maxSize: 255 + year min: 1900 + } + + String toString() { + name + } +} diff --git a/guides/creating-your-first-grails-app/v8/snippets/grails-app/init/example/BootStrap.groovy b/guides/creating-your-first-grails-app/v8/snippets/grails-app/init/example/BootStrap.groovy new file mode 100644 index 00000000000..889304d5c29 --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/snippets/grails-app/init/example/BootStrap.groovy @@ -0,0 +1,27 @@ +package example + +import groovy.transform.CompileStatic + +@CompileStatic +class BootStrap { + + MakeService makeService + ModelService modelService + VehicleService vehicleService + + def init = { + Make nissan = makeService.save('Nissan') + Make ford = makeService.save('Ford') + + Model titan = modelService.save('Titan', nissan) + Model leaf = modelService.save('Leaf', nissan) + Model windstar = modelService.save('Windstar', ford) + + vehicleService.save('Pickup', nissan, titan, 2012) + vehicleService.save('Economy', nissan, leaf, 2014) + vehicleService.save('Minivan', ford, windstar, 1990) + } + + def destroy = { + } +} diff --git a/guides/creating-your-first-grails-app/v8/snippets/grails-app/services/example/MakeService.groovy b/guides/creating-your-first-grails-app/v8/snippets/grails-app/services/example/MakeService.groovy new file mode 100644 index 00000000000..77eda147b60 --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/snippets/grails-app/services/example/MakeService.groovy @@ -0,0 +1,8 @@ +package example + +import grails.gorm.services.Service + +@Service(Make) +interface MakeService { + Make save(String name) +} diff --git a/guides/creating-your-first-grails-app/v8/snippets/grails-app/services/example/ModelService.groovy b/guides/creating-your-first-grails-app/v8/snippets/grails-app/services/example/ModelService.groovy new file mode 100644 index 00000000000..b32fb7966ae --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/snippets/grails-app/services/example/ModelService.groovy @@ -0,0 +1,8 @@ +package example + +import grails.gorm.services.Service + +@Service(Model) +interface ModelService { + Model save(String name, Make make) +} diff --git a/guides/creating-your-first-grails-app/v8/snippets/grails-app/services/example/ValueEstimateService.groovy b/guides/creating-your-first-grails-app/v8/snippets/grails-app/services/example/ValueEstimateService.groovy new file mode 100644 index 00000000000..69c4c06da84 --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/snippets/grails-app/services/example/ValueEstimateService.groovy @@ -0,0 +1,12 @@ +package example + +import grails.gorm.transactions.Transactional + +@Transactional +class ValueEstimateService { + + def getEstimate(Vehicle vehicle) { + // Placeholder — real apps would call a valuation API here + Math.round(vehicle.name.size() + vehicle.model.name.size() * vehicle.year) * 2 + } +} diff --git a/guides/creating-your-first-grails-app/v8/snippets/grails-app/services/example/VehicleService.groovy b/guides/creating-your-first-grails-app/v8/snippets/grails-app/services/example/VehicleService.groovy new file mode 100644 index 00000000000..d670a608972 --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/snippets/grails-app/services/example/VehicleService.groovy @@ -0,0 +1,8 @@ +package example + +import grails.gorm.services.Service + +@Service(Vehicle) +interface VehicleService { + Vehicle save(String name, Make make, Model model, Integer year) +} diff --git a/guides/creating-your-first-grails-app/v8/snippets/grails-app/views/home/index.gsp b/guides/creating-your-first-grails-app/v8/snippets/grails-app/views/home/index.gsp new file mode 100644 index 00000000000..1e228f4226b --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/snippets/grails-app/views/home/index.gsp @@ -0,0 +1,36 @@ + + + + Home Page + + + +
+
+

Welcome ${name}!

+ + +
${flash.message}
+
+ +

There are ${vehicleTotal} vehicles in the database.

+ +
    + +
  • + + ${vehicle.name} - ${vehicle.year} ${vehicle.make.name} ${vehicle.model.name} + +
  • +
    +
+ + + + + +
+
+ + + diff --git a/guides/creating-your-first-grails-app/v8/snippets/grails-app/views/vehicle/create.gsp b/guides/creating-your-first-grails-app/v8/snippets/grails-app/views/vehicle/create.gsp new file mode 100644 index 00000000000..5210162f270 --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/snippets/grails-app/views/vehicle/create.gsp @@ -0,0 +1,43 @@ + + + + + + <g:message code="default.create.label" args="[entityName]"/> + + + + +
+

+ +
${flash.message}
+
+ + + + +
+ +
+
+ +
+
+
+ + diff --git a/guides/creating-your-first-grails-app/v8/snippets/grails-app/views/vehicle/edit.gsp b/guides/creating-your-first-grails-app/v8/snippets/grails-app/views/vehicle/edit.gsp new file mode 100644 index 00000000000..a9c86a094fc --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/snippets/grails-app/views/vehicle/edit.gsp @@ -0,0 +1,45 @@ + + + + + + <g:message code="default.edit.label" args="[entityName]"/> + + + + +
+

+ +
${flash.message}
+
+ + + + + +
+ +
+
+ +
+
+
+ + diff --git a/guides/creating-your-first-grails-app/v8/snippets/grails-app/views/vehicle/index.gsp b/guides/creating-your-first-grails-app/v8/snippets/grails-app/views/vehicle/index.gsp new file mode 100644 index 00000000000..7ff2004c9ef --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/snippets/grails-app/views/vehicle/index.gsp @@ -0,0 +1,29 @@ + + + + + + <g:message code="default.list.label" args="[entityName]"/> + + + + +
+

+ +
${flash.message}
+
+ + +
+ + diff --git a/guides/creating-your-first-grails-app/v8/snippets/grails-app/views/vehicle/show.gsp b/guides/creating-your-first-grails-app/v8/snippets/grails-app/views/vehicle/show.gsp new file mode 100644 index 00000000000..fdfbaa3156c --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/snippets/grails-app/views/vehicle/show.gsp @@ -0,0 +1,41 @@ + + + + + + <g:message code="default.show.label" args="[entityName]"/> + + + + +
+

+

+ Estimated Value: + +

+ +
${flash.message}
+
+ + +
+ + + + +
+
+
+ + diff --git a/guides/creating-your-first-grails-app/v8/snippets/src/integration-test/groovy/example/HomePageSpec.groovy b/guides/creating-your-first-grails-app/v8/snippets/src/integration-test/groovy/example/HomePageSpec.groovy new file mode 100644 index 00000000000..1da7babdb88 --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/snippets/src/integration-test/groovy/example/HomePageSpec.groovy @@ -0,0 +1,18 @@ +package example + +import grails.plugin.geb.ContainerGebSpec +import grails.testing.mixin.integration.Integration + +@Integration +class HomePageSpec extends ContainerGebSpec { + + void 'home page lists seeded vehicles'() { + when: + go '/' + + then: + title == 'Home Page' + $('h1').text().contains('Welcome') + $('li a', text: contains('Pickup')).size() == 1 + } +} diff --git a/guides/creating-your-first-grails-app/v8/snippets/src/test/groovy/example/ValueEstimateServiceSpec.groovy b/guides/creating-your-first-grails-app/v8/snippets/src/test/groovy/example/ValueEstimateServiceSpec.groovy new file mode 100644 index 00000000000..cb465625e36 --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/snippets/src/test/groovy/example/ValueEstimateServiceSpec.groovy @@ -0,0 +1,27 @@ +package example + +import grails.testing.gorm.DataTest +import grails.testing.services.ServiceUnitTest +import spock.lang.Specification + +class ValueEstimateServiceSpec extends Specification + implements ServiceUnitTest, DataTest { + + void setupSpec() { + mockDomains Make, Model, Vehicle + } + + void 'estimate is a sensible positive number'() { + given: + def make = new Make(name: 'Test') + def model = new Model(name: 'Test', make: make) + def vehicle = new Vehicle(year: 2000, make: make, model: model, name: 'Test Vehicle') + + when: + def estimate = service.getEstimate(vehicle) + + then: + estimate > 0 + estimate < 1_000_000 + } +} From 4e405677fee06f89752e9ae85828c7784c96c810 Mon Sep 17 00:00:00 2001 From: Sanjana Date: Wed, 5 Aug 2026 23:15:43 +0530 Subject: [PATCH 2/3] Add url-mappings tag to first-app Grails 8 guide. --- conf/guides.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/conf/guides.yml b/conf/guides.yml index 24c3921defe..92cb6b84c53 100644 --- a/conf/guides.yml +++ b/conf/guides.yml @@ -701,6 +701,7 @@ guides: - 'unit-tests' - 'integration-tests' - 'scaffolding' + - 'url-mappings' - 'getting-started' - 'crud' sampleRef: From 60e8da316da178f83b292debc9634f8d8f8afdfd Mon Sep 17 00:00:00 2001 From: Sanjana Date: Thu, 6 Aug 2026 22:49:24 +0530 Subject: [PATCH 3/3] Add Asset Pipeline chapter to creating-your-first-grails-app v8. --- conf/guides.yml | 3 + .../v8/guide/assetPipeline.adoc | 70 +++++++++++++++++++ .../v8/guide/introduction.adoc | 2 +- .../v8/guide/summary.adoc | 1 + .../assets/javascripts/application.js | 20 ++++++ .../assets/stylesheets/application.css | 14 ++++ 6 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 guides/creating-your-first-grails-app/v8/guide/assetPipeline.adoc create mode 100644 guides/creating-your-first-grails-app/v8/snippets/grails-app/assets/javascripts/application.js create mode 100644 guides/creating-your-first-grails-app/v8/snippets/grails-app/assets/stylesheets/application.css diff --git a/conf/guides.yml b/conf/guides.yml index 92cb6b84c53..bb4c017a283 100644 --- a/conf/guides.yml +++ b/conf/guides.yml @@ -701,6 +701,7 @@ guides: - 'unit-tests' - 'integration-tests' - 'scaffolding' + - 'asset-pipeline' - 'url-mappings' - 'getting-started' - 'crud' @@ -724,6 +725,8 @@ guides: title: Controllers and scaffolding views: title: GSP views + assetPipeline: + title: Asset Pipeline services: title: Services testing: diff --git a/guides/creating-your-first-grails-app/v8/guide/assetPipeline.adoc b/guides/creating-your-first-grails-app/v8/guide/assetPipeline.adoc new file mode 100644 index 00000000000..b206631e0ec --- /dev/null +++ b/guides/creating-your-first-grails-app/v8/guide/assetPipeline.adoc @@ -0,0 +1,70 @@ +The web starter ships with the Asset Pipeline plugin (`cloud.wondrify:asset-pipeline-grails`). It manages CSS, JavaScript, and images under `grails-app/assets/` and exposes them with `` tags. + +Common tags: + +[source,xml] +---- + + + +---- + +Convention: + +* `grails-app/assets/javascripts` for JS +* `grails-app/assets/stylesheets` for CSS +* `grails-app/assets/images` for images + +The default layout already wires the manifests: + +[source,gsp] +.grails-app/views/layouts/main.gsp +---- + + ... +