diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..6c4938f --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,41 @@ +name-template: $RESOLVED_VERSION +tag-template: v$RESOLVED_VERSION +categories: + - title: ✨ Features + labels: + - "type: enhancement" + - "type: new feature" + - "type: major" + - title: 🐛 Bug Fixes/Improvements + labels: + - "type: improvement" + - "type: bug" + - "type: minor" + - title: 🛠 Dependency upgrades + labels: + - "type: dependency upgrade" + - "dependencies" + - title: ⚙️ Build/CI + labels: + - "type: ci" + - "type: build" +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +version-resolver: + major: + labels: + - 'type: major' + minor: + labels: + - 'type: minor' + patch: + labels: + - 'type: patch' + default: patch +template: | + ## What's Changed + + $CHANGES + + ## Contributors + + $CONTRIBUTORS diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 0000000..498bff2 --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,79 @@ +name: Grace CI +on: + push: + branches: + - 'main' + pull_request: + branches: + - 'main' + workflow_dispatch: +jobs: + build: + permissions: + contents: read # to fetch code (actions/checkout) + runs-on: ubuntu-latest + strategy: + matrix: + java: ['11'] + env: + WORKSPACE: ${{ github.workspace }} + steps: + - name: Checkout repository + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'adopt' + java-version: ${{ matrix.java }} + - name: Run Build + id: build + uses: gradle/gradle-build-action@v3 + env: + GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER }} + GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY }} + with: + arguments: build -x test + publish: + if: github.event_name == 'push' + needs: ["build"] + permissions: + contents: read # to fetch code (actions/checkout) + checks: write + runs-on: ubuntu-latest + steps: + - name: Checkout repository + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + distribution: 'adopt' + java-version: 11 + - name: Generate secring file + env: + SECRING_FILE: ${{ secrets.SECRING_FILE }} + run: echo $SECRING_FILE | base64 -d > ${{ github.workspace }}/secring.gpg + - name: Publish to Sonatype OSSRH + id: publish + uses: gradle/gradle-build-action@v3 + env: + GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER }} + GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY: ${{ secrets.GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_NEXUS_URL: ${{ secrets.SONATYPE_NEXUS_URL }} + SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + SIGNING_PASSPHRASE: ${{ secrets.SIGNING_PASSPHRASE }} + SECRING_FILE: ${{ secrets.SECRING_FILE }} + with: + arguments: -Psigning.secretKeyRingFile=${{ github.workspace }}/secring.gpg publishToSonatype closeAndReleaseSonatypeStagingRepository diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml new file mode 100644 index 0000000..031a34c --- /dev/null +++ b/.github/workflows/release-notes.yml @@ -0,0 +1,29 @@ +name: Grace Changelog +on: + issues: + types: [closed,reopened] + push: + branches: + - main + workflow_dispatch: +jobs: + release_notes: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check if it has release drafter config file + id: check_release_drafter + run: | + has_release_drafter=$([ -f .github/release-drafter.yml ] && echo "true" || echo "false") + echo ::set-output name=has_release_drafter::${has_release_drafter} + - name: Extract branch name + id: extract_branch + run: echo ::set-output name=value::${GITHUB_REF:11} + # If it has release drafter: + - uses: release-drafter/release-drafter@v5.19.0 + if: steps.check_release_drafter.outputs.has_release_drafter == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + commitish: ${{ steps.extract_branch.outputs.value }} + filter-by-commitish: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8a68915 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,73 @@ +name: Grace Release + +on: + push: + tags: + - v* + +permissions: + contents: write + +jobs: + create_draft_release: + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Create draft release + run: | + gh release create \ + --repo ${{ github.repository }} \ + --title ${{ github.ref_name }} \ + --notes '' \ + --draft \ + ${{ github.ref_name }} + release_and_publish: + needs: create_draft_release + runs-on: ubuntu-latest + strategy: + matrix: + java: ['11'] + env: + GIT_USER_NAME: rainboyan + GIT_USER_EMAIL: rain@rainboyan.com + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + - uses: gradle/wrapper-validation-action@v2 + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'adopt' + java-version: ${{ matrix.java }} + - name: Extract Target Branch + id: extract_branch + run: | + echo "Determining Target Branch" + TARGET_BRANCH=`cat $GITHUB_EVENT_PATH | jq '.release.target_commitish' | sed -e 's/^"\(.*\)"$/\1/g'` + echo $TARGET_BRANCH + echo ::set-output name=value::${TARGET_BRANCH} + - name: Set the current release version + id: release_version + run: echo "release_version=${GITHUB_REF:11}" >> $GITHUB_OUTPUT + - name: Generate secring file + id: secring + env: + SECRING_FILE: ${{ secrets.SECRING_FILE }} + run: echo $SECRING_FILE | base64 -d > ${{ github.workspace }}/secring.gpg + - name: Publish to Sonatype OSSRH + id: publish + if: steps.secring.outcome == 'success' + uses: gradle/gradle-build-action@v3 + env: + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_NEXUS_URL: ${{ secrets.SONATYPE_NEXUS_URL }} + SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + SIGNING_PASSPHRASE: ${{ secrets.SIGNING_PASSPHRASE }} + SECRING_FILE: ${{ secrets.SECRING_FILE }} + with: + arguments: -Psigning.secretKeyRingFile=${{ github.workspace }}/secring.gpg publishToSonatype closeAndReleaseSonatypeStagingRepository diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f33ace2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +Thumbs.db +.DS_Store +.gradle +build/ +out/ +.idea +*.iml +*.ipr +*.iws +.project +.settings +.classpath diff --git a/README.md b/README.md new file mode 100644 index 0000000..9fdc1ba --- /dev/null +++ b/README.md @@ -0,0 +1,164 @@ +# Grace View Components Plugin + +A Grace plugin for creating reusable, testable and encapsulated view components. + +## Grace Version + +- Grace **2022.0.0** + +## Usage + +### Add dependency `view-components` + +Adding `view-components` plugin to the `build.gradle`, + +```gradle +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath "org.graceframework:grace-gradle-plugin:$grailsVersion" + classpath "org.graceframework.plugins:views-gradle:5.1.0" + } +} + +apply plugin: "org.graceframework.grace-gsp" +apply plugin: "org.graceframework.plugins.views-markup" // Need to build Grace Markup Views + +repositories { + mavenCentral() +} + +dependencies { + implementation "org.graceframework.plugins:view-components:0.0.1" +} + +``` + +Let's start create some Components, `ButtonComponent` in `app/components`, + +```bash +. +├── app +│   ├── assets +│   ├── components +│   │   └── demo +│   │     ├── ButtonComponent.groovy +│   │     └── CardComponent.groovy +│   └── views +│      └── components +│         ├── button +│         │ └── button_component.gml +│         └── card +│         └── card_component.gml + +``` + +`ButtonComponent` is just a POGO, we define attribues which will be used in Markup views. + +```groovy +class ButtonComponent { + String name = 'Button' + String type = 'button' + String size + String cssClasses + String color + String state + String icon + + String getCssClasses() { + String theCssClasses = 'btn' + if (this.cssClasses) { + theCssClasses += ' ' + this.cssClasses + } + else { + theCssClasses += " btn-info" + } + if (size) { + theCssClasses += " btn-$size" + } + theCssClasses + } +} +``` + +In the `app/views/components/button/button_component.gml`, + +```html +model { + String name + String type + String size + String cssClasses + String color + String state + String icon +} + +button([type: type, class: cssClasses] + (state == 'disabled' ? [disabled : ''] : [:]) + (color ? [style: 'color: ' + color] : [:])) { + if (icon) { + i(class: "bi bi-${icon}") { + } + } + yield name +} +``` + +Using the `ButtonComponent`, `CardComponent` in your GSPs, it's very easy, `ViewComponents` support custom namespace and tags. + +```html + +// Using expression in GSP +${new ButtonComponent(name: 'Primary Button', cssClasses: 'btn-primary').render()} + +// Using tag in GSP + +``` + +### Using Inline template + +You also can write template in Component groovy source using `inline(String templateText)`, it's One File Component! + +```groovy +class IconComponent { + String name + + def render() { + inline """ +i(class: "bi bi-$name") { +} +""" + } +} +``` + +## Development + +### Build from source + +``` +git clone https://github.com/graceframework/grace-view-components.git +cd grace-view-components +./gradlew publishToMavenLocal +``` + +## What's New + +### 0.0.1 + +* Support Grace 2022.0+ +* Introduce View Components, using in Controller and GSP +* New taglib: ComponentTagLib + + +## License + +This plugin is available as open source under the terms of the [APACHE LICENSE, VERSION 2.0](http://apache.org/Licenses/LICENSE-2.0) + +## Links + +- [Grace Website](https://github.com/graceframework/grace-framework) +- [Grace Plugins](https://github.com/grace-plugins) +- [Grace View Components Plugin](https://github.com/grace-plugins/grace-view-components) +- [Grace View Components Guide](https://github.com/grace-guides/gs-view-components) diff --git a/app/conf/application.yml b/app/conf/application.yml new file mode 100644 index 0000000..6182b71 --- /dev/null +++ b/app/conf/application.yml @@ -0,0 +1,44 @@ +grails: + profile: web-plugin + codegen: + defaultPackage: org.graceframework.plugin.components + gorm: + reactor: + # Whether to translate GORM events into Reactor events + # Disabled by default for performance reasons + events: false +info: + app: + name: '@info.app.name@' + version: '@info.app.version@' + grailsVersion: '@info.app.grailsVersion@' +spring: + jmx: + unique-names: true + main: + banner-mode: "console" + groovy: + template: + check-template-location: false + devtools: + restart: + additional-exclude: + - '*.gsp' + - '**/*.gsp' + - '*.gson' + - '**/*.gson' + - 'logback.groovy' + - '*.properties' +environments: + development: + management: + endpoints: + enabled-by-default: true + web: + base-path: '/actuator' + exposure: + include: '*' + production: + management: + endpoints: + enabled-by-default: false diff --git a/app/conf/logback.xml b/app/conf/logback.xml new file mode 100644 index 0000000..66513f1 --- /dev/null +++ b/app/conf/logback.xml @@ -0,0 +1,19 @@ + + + + + + + + + UTF-8 + %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wex + + + + + + + + + diff --git a/app/init/org/graceframework/plugin/components/Application.groovy b/app/init/org/graceframework/plugin/components/Application.groovy new file mode 100644 index 0000000..921a6c9 --- /dev/null +++ b/app/init/org/graceframework/plugin/components/Application.groovy @@ -0,0 +1,13 @@ +package org.graceframework.plugin.components + +import grails.boot.Grails +import grails.plugins.metadata.PluginSource + +@PluginSource +class Application { + + static void main(String[] args) { + Grails.run(Application, args) + } + +} diff --git a/app/init/org/graceframework/plugin/components/BootStrap.groovy b/app/init/org/graceframework/plugin/components/BootStrap.groovy new file mode 100644 index 0000000..66ee71e --- /dev/null +++ b/app/init/org/graceframework/plugin/components/BootStrap.groovy @@ -0,0 +1,11 @@ +package org.graceframework.plugin.components + +class BootStrap { + + def init = { servletContext -> + } + + def destroy = { + } + +} diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..378c738 --- /dev/null +++ b/build.gradle @@ -0,0 +1,147 @@ +buildscript { + repositories { + // mavenLocal() + mavenCentral() + maven { url "https://plugins.gradle.org/m2/" } + maven { url 'https://repo.gradle.org/gradle/libs-releases' } + } + dependencies { + classpath "org.graceframework:grace-gradle-plugin:$graceVersion" + classpath "io.github.gradle-nexus:publish-plugin:1.3.0" + } +} + +ext."signing.keyId" = project.hasProperty("signing.keyId") ? project.getProperty('signing.keyId') : System.getenv('SIGNING_KEY') +ext."signing.password" = project.hasProperty("signing.password") ? project.getProperty('signing.password') : System.getenv('SIGNING_PASSPHRASE') +ext."signing.secretKeyRingFile" = project.hasProperty("signing.secretKeyRingFile") ? project.getProperty('signing.secretKeyRingFile') : ("${System.properties['user.home']}${File.separator}.gnupg${File.separator}secring.gpg") +ext.isReleaseVersion = !projectVersion.endsWith("SNAPSHOT") + +version projectVersion +group "org.graceframework.plugins" + +apply plugin: "eclipse" +apply plugin: "idea" +apply plugin: "java-library" +apply plugin: "org.graceframework.grace-plugin" +apply plugin: "io.github.gradle-nexus.publish-plugin" +apply plugin: "maven-publish" +apply plugin: "signing" + +repositories { + // mavenLocal() + mavenCentral() +} + +dependencies { + compileOnly "org.springframework.boot:spring-boot-autoconfigure" + compileOnly "org.graceframework:grace-boot" + compileOnly "org.graceframework:grace-core" + compileOnly "org.graceframework:grace-web-common" + compileOnly "org.graceframework.plugins:views-markup:5.1.0" + implementation "commons-beanutils:commons-beanutils:1.9.4" + profile "org.graceframework.profiles:web-plugin" +} + +tasks.withType(Sign) { + onlyIf { isReleaseVersion } +} + +tasks.withType(GroovyCompile) { + configure(groovyOptions) { + forkOptions.jvmArgs = ['-Xmx1024m'] + } +} + +tasks.withType(Test) { + useJUnitPlatform() +} + +bootJar.enabled = false + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(11) + } + withJavadocJar() + withSourcesJar() +} + +jar { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + manifest.mainAttributes( + "Built-By": System.properties['user.name'], + "Created-By": System.properties['java.vm.version'] + " (" + System.properties['java.vm.vendor'] + ")", + "Implementation-Title": "Grace View Components", + "Implementation-Version": projectVersion, + "Implementation-Vendor": 'Grace Plugins') + enabled = true + archiveClassifier.set('') + includeEmptyDirs = false +} + +publishing { + publications { + maven(MavenPublication) { + groupId = project.group + artifactId = project.name + version = project.version + + versionMapping { + usage('java-api') { + fromResolutionOf('runtimeClasspath') + } + usage('java-runtime') { + fromResolutionResult() + } + } + + from components.java + + pom { + name = "Grace View Components" + description = "A Grace plugin for creating reusable, testable and encapsulated view components." + url = 'https://github.com/grace-plugin/grace-view-components' + licenses { + license { + name = 'The Apache License, Version 2.0' + url = 'https://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + developers { + developer { + id = 'rainboyan' + name = 'Michael Yan' + email = 'rain@rainboyan.com' + } + } + scm { + connection = 'scm:git:git://github.com/grace-plugin/grace-view-components.git' + developerConnection = 'scm:git:ssh://github.com:grace-plugin/grace-view-components.git' + url = 'https://github.com/grace-plugin/grace-view-components/tree/main' + } + } + } + } +} + +nexusPublishing { + repositories { + sonatype { + def ossUser = System.getenv("SONATYPE_USERNAME") ?: project.hasProperty("sonatypeOssUsername") ? project.sonatypeOssUsername : '' + def ossPass = System.getenv("SONATYPE_PASSWORD") ?: project.hasProperty("sonatypeOssPassword") ? project.sonatypeOssPassword : '' + def ossStagingProfileId = System.getenv("SONATYPE_STAGING_PROFILE_ID") ?: project.hasProperty("sonatypeOssStagingProfileId") ? project.sonatypeOssStagingProfileId : '' + nexusUrl = uri("https://s01.oss.sonatype.org/service/local/") + snapshotRepositoryUrl = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") + username = ossUser + password = ossPass + stagingProfileId = ossStagingProfileId + } + } +} + +afterEvaluate { + signing { + required { isReleaseVersion && gradle.taskGraph.hasTask("publish") } + sign publishing.publications.maven + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..1b9bcf0 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,6 @@ +projectVersion=0.0.1-SNAPSHOT +graceVersion=2022.1.0 +groovyVersion=3.0.16 +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx1024M diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..afba109 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..b1624c4 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..65dcd68 --- /dev/null +++ b/gradlew @@ -0,0 +1,244 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100755 index 0000000..6689b85 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..41652b7 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'view-components' diff --git a/src/main/groovy/grails/compiler/traits/ComponentRenderer.groovy b/src/main/groovy/grails/compiler/traits/ComponentRenderer.groovy new file mode 100644 index 0000000..33449bb --- /dev/null +++ b/src/main/groovy/grails/compiler/traits/ComponentRenderer.groovy @@ -0,0 +1,42 @@ +/* + * Copyright 2022-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package grails.compiler.traits + +import groovy.transform.Generated + +import grails.views.Component + +/** + * Component render + * + * @author Michael Yan + * @since 0.0.1 + */ +trait ComponentRenderer { + + @Generated + void render(Component component) { + render(component, [:]) + } + + @Generated + void render(Component component, Map args) { + Map argMap = [text: component.render(), contentType: "text/html", encoding: "UTF-8"] + argMap.putAll(args) + render(argMap) + } + +} \ No newline at end of file diff --git a/src/main/groovy/grails/compiler/traits/ComponentTraitInjector.groovy b/src/main/groovy/grails/compiler/traits/ComponentTraitInjector.groovy new file mode 100644 index 0000000..5cec42a --- /dev/null +++ b/src/main/groovy/grails/compiler/traits/ComponentTraitInjector.groovy @@ -0,0 +1,41 @@ +/* + * Copyright 2022-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package grails.compiler.traits + +import groovy.transform.CompileStatic + +import grails.views.Component + +/** + * Component TraitInjector + * + * @author Michael Yan + * @since 0.0.1 + */ +@CompileStatic +class ComponentTraitInjector implements TraitInjector { + + @Override + Class getTrait() { + Component + } + + @Override + String[] getArtefactTypes() { + ['Component'] as String[] + } + +} diff --git a/src/main/groovy/grails/compiler/traits/ControllerComponentTraitInjector.groovy b/src/main/groovy/grails/compiler/traits/ControllerComponentTraitInjector.groovy new file mode 100644 index 0000000..4c9fd77 --- /dev/null +++ b/src/main/groovy/grails/compiler/traits/ControllerComponentTraitInjector.groovy @@ -0,0 +1,39 @@ +/* + * Copyright 2022-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package grails.compiler.traits + +import groovy.transform.CompileStatic + +/** + * Controller TraitInjector for Component + * + * @author Michael Yan + * @since 0.0.1 + */ +@CompileStatic +class ControllerComponentTraitInjector implements TraitInjector { + + @Override + Class getTrait() { + ComponentRenderer + } + + @Override + String[] getArtefactTypes() { + ['Controller'] as String[] + } + +} diff --git a/src/main/groovy/grails/views/Component.groovy b/src/main/groovy/grails/views/Component.groovy new file mode 100644 index 0000000..8f157e2 --- /dev/null +++ b/src/main/groovy/grails/views/Component.groovy @@ -0,0 +1,116 @@ +/* + * Copyright 2022-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package grails.views + +import groovy.text.Template +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import groovy.transform.Generated +import org.codehaus.groovy.runtime.DefaultGroovyMethods +import org.codehaus.groovy.runtime.InvokerHelper +import org.springframework.beans.factory.annotation.Autowired + +import grails.plugin.markup.view.MarkupViewTemplateEngine +import grails.util.GrailsNameUtils +import grails.web.api.WebAttributes + +import org.grails.buffer.FastStringWriter +import org.grails.encoder.Encoder +import org.grails.taglib.encoder.WithCodecHelper +import org.graceframework.plugin.components.artefact.ComponentArtefactHandler +import org.graceframework.plugin.util.StringUtils + +/** + * Component trait + * + * @author Michael Yan + * @since 0.0.1 + */ +@CompileStatic +trait Component extends WebAttributes { + public static final String COMPONENT_VIEW_DIR = '/components' + + MarkupViewTemplateEngine markupTemplateEngine + private Encoder rawEncoder + + @Generated + @Autowired(required = false) + void setComponentTemplateEngine(MarkupViewTemplateEngine markupTemplateEngine) { + this.markupTemplateEngine = markupTemplateEngine + } + + @Generated + MarkupViewTemplateEngine getComponentTemplateEngine() { + this.markupTemplateEngine ?: grailsAttributes.applicationContext.getBean('markupTemplateEngine', MarkupViewTemplateEngine) + } + + @Generated + String getComponentName() { + GrailsNameUtils.getLogicalPropertyName(getClass().name, ComponentArtefactHandler.TYPE) + } + + @Generated + Template getTemplate() { + getTemplate(null) + } + + @Generated + Template getTemplate(String templateText) { + if (templateText) { + return getComponentTemplateEngine().createTemplate(templateText) + } + String viewName = StringUtils.getSnakeCaseName(getClass()) + String templatePath = COMPONENT_VIEW_DIR + '/' + viewName + WritableScriptTemplate template = getComponentTemplateEngine().resolveTemplate(templatePath) + if (!template) { + templatePath = COMPONENT_VIEW_DIR + '/' + getComponentName() + '/' + viewName + template = getComponentTemplateEngine().resolveTemplate(templatePath) + } + template + } + + @Generated + def render() { + render(null) + } + + @Generated + String render(String templateText) { + Map binding = new HashMap<>() + binding.putAll(DefaultGroovyMethods.getProperties(this)) + FastStringWriter writer = new FastStringWriter() + getTemplate(templateText).make(binding).writeTo(writer) + raw(writer.toString()) + } + + @Generated + String inline(String templateText) { + render(templateText) + } + + @Generated + @CompileDynamic + def raw(Object value) { + if (this.rawEncoder == null) { + this.rawEncoder = WithCodecHelper.lookupEncoder(grailsApplication, 'Raw') + if (this.rawEncoder == null) { + return InvokerHelper.invokeMethod(value, 'encodeAsRaw', null) + } + } + this.rawEncoder.encode(value) + } + +} diff --git a/src/main/groovy/grails/views/GrailsComponentClass.java b/src/main/groovy/grails/views/GrailsComponentClass.java new file mode 100644 index 0000000..6cab979 --- /dev/null +++ b/src/main/groovy/grails/views/GrailsComponentClass.java @@ -0,0 +1,29 @@ +/* + * Copyright 2022-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package grails.views; + +import grails.core.InjectableGrailsClass; + +/** + * Represents a controller class in Grails. + * + * @author Michael Yan + * + * @since 0.0.1 + */ +public interface GrailsComponentClass extends InjectableGrailsClass { + +} diff --git a/src/main/groovy/org/graceframework/plugin/components/ViewComponentsGrailsPlugin.groovy b/src/main/groovy/org/graceframework/plugin/components/ViewComponentsGrailsPlugin.groovy new file mode 100644 index 0000000..896e90b --- /dev/null +++ b/src/main/groovy/org/graceframework/plugin/components/ViewComponentsGrailsPlugin.groovy @@ -0,0 +1,76 @@ +package org.graceframework.plugin.components + +import grails.plugins.* + +import org.graceframework.plugin.components.taglib.ComponentTagLib + +class ViewComponentsGrailsPlugin extends Plugin { + + // the version or versions of Grails the plugin is designed for + def grailsVersion = "2022.0.0 > *" + // resources that are excluded from plugin packaging + def pluginExcludes = [ + "grails-app/views/error.gsp" + ] + def dependsOn = [markupView: '1.0.0 > *'] + + def watchedResources = ["file:./app/components/**/*Component.groovy", + "file:./plugins/*/app/components/**/*Component.groovy"] + def providedArtefacts = [ + ComponentTagLib + ] + + // TODO Fill in these fields + def title = "Grace View Components" // Headline display name of the plugin + def author = "Michael Yan" + def authorEmail = "rain@rainboyan.com" + def description = '''\ +A Grace plugin for creating reusable, testable and encapsulated view components. +''' + def profiles = ['web'] + + // URL to the plugin's documentation + def documentation = "https://github.com/grace-plugin/grace-view-components" + + // Extra (optional) plugin metadata + + // License: one of 'APACHE', 'GPL2', 'GPL3' + def license = "APACHE" + + // Any additional developers beyond the author specified above. + def developers = [ [id: "rainboyan", name: "Michael Yan", email: "rain@rainboyan.com" ]] + + // Location of the plugin's issue tracker. + def issueManagement = [ system: "GitHub", url: "https://github.com/grace-plugins/grace-view-components/issues" ] + + // Online location of the plugin's browseable source code. + def scm = [ url: "https://github.com/grace-plugins/grace-view-components" ] + + Closure doWithSpring() { {-> + // TODO Implement runtime spring config (optional) + } + } + + void doWithDynamicMethods() { + // TODO Implement registering dynamic methods to classes (optional) + } + + void doWithApplicationContext() { + // TODO Implement post initialization spring config (optional) + } + + void onChange(Map event) { + // TODO Implement code that is executed when any artefact that this plugin is + // watching is modified and reloaded. The event contains: event.source, + // event.application, event.manager, event.ctx, and event.plugin. + } + + void onConfigChange(Map event) { + // TODO Implement code that is executed when the project configuration changes. + // The event is the same as for 'onChange'. + } + + void onShutdown(Map event) { + // TODO Implement code that is executed when the application shuts down (optional) + } +} diff --git a/src/main/groovy/org/graceframework/plugin/components/artefact/ComponentArtefactHandler.java b/src/main/groovy/org/graceframework/plugin/components/artefact/ComponentArtefactHandler.java new file mode 100644 index 0000000..b0018b9 --- /dev/null +++ b/src/main/groovy/org/graceframework/plugin/components/artefact/ComponentArtefactHandler.java @@ -0,0 +1,45 @@ +/* + * Copyright 2022-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.graceframework.plugin.components.artefact; + +import grails.core.ArtefactHandlerAdapter; +import grails.views.GrailsComponentClass; +import org.graceframework.plugin.components.component.DefaultGrailsComponentClass; + +/** + * Grails View Component. + * + *

This class is responsible for looking up component classes for views.

+ * + * @author Michael Yan + * @since 0.0.1 +*/ +public class ComponentArtefactHandler extends ArtefactHandlerAdapter { + + public static final String TYPE = "Component"; + public static final String PLUGIN_NAME = "viewComponents"; + + public ComponentArtefactHandler() { + super(TYPE, GrailsComponentClass.class, DefaultGrailsComponentClass.class, + DefaultGrailsComponentClass.COMPONENT, false); + } + + @Override + public String getPluginName() { + return PLUGIN_NAME; + } + +} diff --git a/src/main/groovy/org/graceframework/plugin/components/component/DefaultGrailsComponentClass.java b/src/main/groovy/org/graceframework/plugin/components/component/DefaultGrailsComponentClass.java new file mode 100644 index 0000000..6b44b1f --- /dev/null +++ b/src/main/groovy/org/graceframework/plugin/components/component/DefaultGrailsComponentClass.java @@ -0,0 +1,35 @@ +/* + * Copyright 2022-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.graceframework.plugin.components.component; + +import grails.views.GrailsComponentClass; +import org.grails.core.AbstractInjectableGrailsClass; + +/** + * Default GrailsComponentClass + * + * @author Michael Yan + * @since 0.0.1 + */ +public class DefaultGrailsComponentClass extends AbstractInjectableGrailsClass implements GrailsComponentClass { + + public static final String COMPONENT = "Component"; + + public DefaultGrailsComponentClass(Class clazz) { + super(clazz, COMPONENT); + } + +} \ No newline at end of file diff --git a/src/main/groovy/org/graceframework/plugin/components/taglib/ComponentTagLib.groovy b/src/main/groovy/org/graceframework/plugin/components/taglib/ComponentTagLib.groovy new file mode 100644 index 0000000..4b93118 --- /dev/null +++ b/src/main/groovy/org/graceframework/plugin/components/taglib/ComponentTagLib.groovy @@ -0,0 +1,104 @@ +/* + * Copyright 2022-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.graceframework.plugin.components.taglib + +import groovy.transform.CompileStatic +import org.apache.commons.beanutils.BeanUtils as CommonsBeanUtils +import org.springframework.beans.BeanUtils as SpringBeanUtils +import org.springframework.beans.factory.InitializingBean +import org.springframework.context.ApplicationContext +import org.springframework.context.ApplicationContextAware + +import grails.artefact.TagLibrary +import grails.core.GrailsApplication +import grails.core.GrailsClass +import grails.core.support.GrailsApplicationAware +import grails.gsp.TagLib +import grails.util.GrailsNameUtils +import grails.views.Component + +import org.graceframework.plugin.components.artefact.ComponentArtefactHandler + +/** + * Component TagLib + * + * @author Michael Yan + * @since 0.0.1 + */ +@CompileStatic +@TagLib +class ComponentTagLib implements ApplicationContextAware, GrailsApplicationAware, InitializingBean, TagLibrary { + + static namespace = 'vc' + static defaultEncodeAs = 'raw' + + GrailsApplication grailsApplication + ApplicationContext applicationContext + + private final Map components = new HashMap<>() + + @Override + void afterPropertiesSet() { + GrailsClass[] componentClasses = grailsApplication.getArtefacts(ComponentArtefactHandler.TYPE) + componentClasses.each { + String componentName = GrailsNameUtils.getLogicalPropertyName(it.clazz.name, ComponentArtefactHandler.TYPE) + this.components.put(componentName, it.clazz) + } + } + + /** + * Renders a component. Examples:
+ * + * <vc:render component="${new ButtonComponent(name: 'Create')}" />
+ * + * @attr component REQUIRED The component to render + */ + Closure render = { Map attrs, body -> + def component = attrs.component + Component componentObject + Class componentClass + String componentName + if (component instanceof Component) { + componentClass = component.class + componentObject = component as Component + componentName = GrailsNameUtils.getLogicalPropertyName(componentClass.name, ComponentArtefactHandler.TYPE) + } + else { + componentClass = this.components.get(component) + componentName = component + if (componentClass) { + componentObject = SpringBeanUtils.instantiateClass(componentClass, Component) + if (componentObject) { + def props = attrs.model ?: attrs + if (props) { + CommonsBeanUtils.copyProperties(componentObject, (Map) props) + } + } + } + else { + componentObject = null + } + } + if (componentObject && componentClass) { + out.write(componentObject.render().toString()) + } + else { + throwTagError("Component with name [\"$componentName\"] not found") + } + null + } + +} diff --git a/src/main/groovy/org/graceframework/plugin/util/StringUtils.java b/src/main/groovy/org/graceframework/plugin/util/StringUtils.java new file mode 100644 index 0000000..9b1a580 --- /dev/null +++ b/src/main/groovy/org/graceframework/plugin/util/StringUtils.java @@ -0,0 +1,56 @@ +/* + * Copyright 2022-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.graceframework.plugin.util; + +import static grails.util.GrailsNameUtils.getNaturalName; + +/** + * StringUtils + * + * @author Michael Yan + * @since 0.0.1 + */ +public final class StringUtils { + + /** + * Retrieves the snake case name of the supplied class. + * For example MyFunkyGrailsScript would be my_funky_grails_script. + * + * @param clazz The class to convert + * @return The script name representation + */ + public static String getSnakeCaseName(Class clazz) { + return clazz == null ? null : getSnakeCaseName(clazz.getName()); + } + + /** + * Retrieves the snake case name of the given class name. + * For example MyFunkyGrailsScript would be my_funky_grails_script. + * + * @param name The class name to convert. + * @return The snake case name representation. + */ + public static String getSnakeCaseName(String name) { + if (name == null) { + return null; + } + + if (name.endsWith(".groovy")) { + name = name.substring(0, name.length() - 7); + } + return getNaturalName(name).replaceAll("\\s", "_").toLowerCase(); + } +} diff --git a/src/main/resources/META-INF/grails-plugin.xml b/src/main/resources/META-INF/grails-plugin.xml new file mode 100644 index 0000000..d87dc73 --- /dev/null +++ b/src/main/resources/META-INF/grails-plugin.xml @@ -0,0 +1,3 @@ + + org.graceframework.plugin.components.ViewComponentsGrailsPlugin + \ No newline at end of file diff --git a/src/main/resources/META-INF/grails.factories b/src/main/resources/META-INF/grails.factories new file mode 100644 index 0000000..4d10304 --- /dev/null +++ b/src/main/resources/META-INF/grails.factories @@ -0,0 +1,2 @@ +grails.compiler.traits.TraitInjector=grails.compiler.traits.ComponentTraitInjector,grails.compiler.traits.ControllerComponentTraitInjector +grails.core.ArtefactHandler=org.graceframework.plugin.components.artefact.ComponentArtefactHandler \ No newline at end of file