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