Skip to content

Commit

Permalink
✨ : manage CSS & JS external dependencies dynamically
Browse files Browse the repository at this point in the history
  • Loading branch information
juwit committed Dec 19, 2019
1 parent 3f0a542 commit 037ec7c
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package io.codeka.gaia.dashboard.controller

import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ModelAttribute

/***
* A controller advice to inject ui-extensions
*/
@ControllerAdvice
class UIExtensionsControllerAdvice(uiExtensions: List<UiExtension>) {

private var cssExtensions: List<UiExtension> = uiExtensions.filter { it.resourcePath.endsWith(".css") }

private var jsExtensions: List<UiExtension> = uiExtensions.filter { it.resourcePath.endsWith(".js") }

@ModelAttribute(value = "cssExtensions", binding = false)
fun getCssExtensions() = this.cssExtensions

@ModelAttribute(value = "jsExtensions", binding = false)
fun getJsExtensions() = this.jsExtensions

}

data class UiExtension(val resourcePath: String)

@Configuration
open class UIExtensionsConfig(){

@Bean
open fun uiExtensions(applicationContext: ApplicationContext): List<UiExtension>? {
val scanner = UiExtensionsScanner(applicationContext)
return scanner.scan(
// CSS
"classpath*:/**/bootstrap/**/bootstrap.min.css",
"classpath*:/**/font-awesome/**/all.css",
"classpath*:/**/bootstrap-vue/**/bootstrap-vue.min.css",
"classpath*:/**/vue-multiselect/**/vue-multiselect.min.css",

// JS
"classpath*:/**/jquery/**/jquery.min.js",
"classpath*:/**/popper.js/**/umd/popper.min.js",
"classpath*:/**/bootstrap/**/bootstrap.min.js",
"classpath*:/**/vue/**/vue.min.js",
"classpath*:/**/bootstrap-vue/**/bootstrap-vue.min.js",
"classpath*:/**/marked/**/marked.min.js",
"classpath*:/**/vue-multiselect/**/vue-multiselect.min.js",
"classpath*:/**/momentjs/**/moment.min.js",
"classpath*:/**/moment-duration-format/**/moment-duration-format.js"
)
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.codeka.gaia.dashboard.controller

import org.springframework.core.io.Resource
import org.springframework.core.io.support.ResourcePatternResolver

class UiExtensionsScanner(private val resolver: ResourcePatternResolver) {

fun scan(vararg locations: String): List<UiExtension> {
return locations
.map { resolveAsset(it) }
.filter { it.isReadable }
.map { getResourcePath(it) }
.map { UiExtension(it) }
}

/**
* Resolves a wildcard location to a classpath Resource
*/
private fun resolveAsset(location: String): Resource {
return resolver.getResources(location).single()
}

/**
* Get a resource path to be included in html (from /webjars)
*/
private fun getResourcePath(resource: Resource): String {
return resource.url.toString().replaceBeforeLast("/webjars","")
}

}
12 changes: 4 additions & 8 deletions src/main/resources/templates/layout/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,10 @@
<meta name="description" content="">
<meta name="author" content="">

<!-- bootstrap css -->
<link rel="stylesheet" href="/webjars/bootstrap/4.3.1/css/bootstrap.min.css" />

<link rel="stylesheet" href="/webjars/font-awesome/5.8.2/css/all.css" />

<link rel="stylesheet" href="/webjars/bootstrap-vue/2.0.4/dist/bootstrap-vue.css" />

<link rel="stylesheet" href="/webjars/vue-multiselect/2.1.6/dist/vue-multiselect.min.css" />
<!-- external CSS dependencies -->
<th:block th:each="cssFile : ${cssExtensions}">
<link th:href="${cssFile.resourcePath}" rel="stylesheet">
</th:block>

<!-- custom css -->
<link rel="stylesheet" href="/css/style.css" />
Expand Down
17 changes: 4 additions & 13 deletions src/main/resources/templates/layout/include_scripts.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
<!-- javascript load -->
<!-- jquery & bootstrap -->
<script src="/webjars/jquery/3.0.0/jquery.min.js"></script>
<script src="/webjars/popper.js/1.14.3/umd/popper.min.js"></script>
<script src="/webjars/bootstrap/4.3.1/js/bootstrap.min.js"></script>
<!-- vue & bootstrap-vue -->
<script src="/webjars/vue/2.5.16/vue.js"></script>
<script src="/webjars/bootstrap-vue/2.0.4/dist/bootstrap-vue.js"></script>
<!-- other components -->
<script src="/webjars/marked/0.6.2/marked.min.js"></script>
<script src="/webjars/vue-multiselect/2.1.6/dist/vue-multiselect.min.js"></script>
<script src="/webjars/momentjs/2.24.0/min/moment.min.js"></script>
<script src="/webjars/moment-duration-format/1.3.0/lib/moment-duration-format.js"></script>
<!-- external JS dependencies-->
<th:block th:each="jsFile : ${jsExtensions}">
<script th:src="${jsFile.resourcePath}"></script>
</th:block>
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package io.codeka.gaia.dashboard.controller

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.rnorth.visibleassertions.VisibleAssertions.assertThrows
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.ApplicationContext
import java.util.NoSuchElementException

@SpringBootTest(classes = [UIExtensionsConfig::class])
class UiExtensionsScannerTest(val applicationContext: ApplicationContext) {

@Test
fun `scan should find all exsting extensions`() {
val scanner = UiExtensionsScanner(applicationContext)

val locations = arrayOf(
// CSS
"classpath*:/**/bootstrap/**/bootstrap.min.css",
"classpath*:/**/font-awesome/**/all.css",
"classpath*:/**/bootstrap-vue/**/bootstrap-vue.min.css",
"classpath*:/**/vue-multiselect/**/vue-multiselect.min.css",

// JS
"classpath*:/**/jquery/**/jquery.min.js",
"classpath*:/**/popper.js/**/umd/popper.min.js",
"classpath*:/**/bootstrap/**/bootstrap.min.js",
"classpath*:/**/vue/**/vue.min.js",
"classpath*:/**/bootstrap-vue/**/bootstrap-vue.min.js",
"classpath*:/**/marked/**/marked.min.js",
"classpath*:/**/vue-multiselect/**/vue-multiselect.min.js",
"classpath*:/**/momentjs/**/moment.min.js",
"classpath*:/**/moment-duration-format/**/moment-duration-format.js"
)

val uiExtension = scanner.scan(*locations)

assertThat(uiExtension.size).isEqualTo(locations.size);
}

@Test
fun `scan should throw an exception when an extension cannot be found`() {
val scanner = UiExtensionsScanner(applicationContext)

val locations = arrayOf("classpath*:/**/bootstrap/**/bootstrap.min.BOUM")

assertThrows("Array is empty", NoSuchElementException::class.java) { scanner.scan(*locations) }
}
}

0 comments on commit 037ec7c

Please sign in to comment.