├── .nojekyll ├── .gitignore ├── docs ├── styles.css ├── posts │ ├── 03_basic_routing │ │ ├── get-home.png │ │ ├── post-home.png │ │ ├── put-user.png │ │ └── delete-user.png │ ├── 14_cors │ │ └── cors_server_flowchart.png │ └── 04_simple_json_api │ │ └── screenshots │ │ ├── get-api-members.png │ │ ├── get-api-member-id.png │ │ ├── post-api-members.png │ │ ├── put-api-member-id.png │ │ ├── delete-api-member-id.png │ │ ├── get-api-member-id-not-found.png │ │ └── post-api-members-required-fields.png ├── site_libs │ ├── bootstrap │ │ └── bootstrap-icons.woff │ └── quarto-html │ │ └── tippy.css └── listings.json ├── 05_router ├── box │ ├── .Renviron │ ├── .Rprofile │ ├── renv │ │ ├── .gitignore │ │ └── settings.json │ ├── data │ │ ├── members.sqlite │ │ └── get_db_path.R │ ├── routes │ │ └── api │ │ │ ├── members │ │ │ ├── get_db_path.R │ │ │ ├── controllers.R │ │ │ ├── get_all_members.R │ │ │ ├── get_member_by_id.R │ │ │ ├── delete_member.R │ │ │ ├── create_new_member.R │ │ │ └── update_member_info.R │ │ │ └── members.R │ ├── server.R │ ├── middleware │ │ └── logger.R │ ├── helpers │ │ ├── get_port.R │ │ ├── check_port.R │ │ └── operators.R │ ├── models │ │ └── members.R │ └── README.md ├── r_pkg_structure │ ├── .Renviron │ ├── .Rbuildignore │ ├── .Rprofile │ ├── server.R │ ├── renv │ │ ├── .gitignore │ │ └── settings.json │ ├── R │ │ ├── get_all_members.R │ │ ├── app.R │ │ ├── get_port.R │ │ ├── members.R │ │ ├── members_router.R │ │ ├── check_port.R │ │ ├── get_member_by_id.R │ │ ├── operators.R │ │ ├── delete_member.R │ │ ├── create_new_member.R │ │ └── update_member_info.R │ ├── DESCRIPTION │ └── README.md └── README.md ├── 09_goals ├── .Rprofile ├── .gitignore ├── renv │ ├── .gitignore │ └── settings.json ├── .Renviron ├── models │ ├── goal_model.R │ └── user_model.R ├── helpers │ ├── mongo_query.R │ ├── get_jwt_secret.R │ ├── get_port.R │ ├── generate_token.R │ ├── to_json.R │ ├── operators.R │ ├── truthiness.R │ └── insert.R ├── config │ └── db.R ├── server.R ├── routes │ ├── goal_routes.R │ └── user_routes.R ├── middleware │ ├── error_middleware.R │ └── auth_middleware.R ├── README.md └── controllers │ └── goal_controller.R ├── 10_live_reloading ├── .gitignore ├── .Rprofile ├── renv │ ├── .gitignore │ └── settings.json ├── nodemon.json ├── server.R ├── package.json └── README.md ├── 14_cors ├── .Rprofile ├── renv │ ├── .gitignore │ └── settings.json ├── README.md └── server.R ├── 15_chat ├── .Rprofile ├── ui │ ├── __init__.R │ └── page.R ├── renv │ ├── .gitignore │ └── settings.json ├── chat │ ├── __init__.R │ └── chat.R ├── index.R └── public │ ├── style.css │ ├── ambiorix.js │ └── chat.js ├── documentation ├── .gitignore ├── styles.css ├── profile.jpg ├── posts │ ├── 03_basic_routing │ │ ├── get-home.png │ │ ├── post-home.png │ │ ├── put-user.png │ │ ├── delete-user.png │ │ └── index.qmd │ ├── 14_cors │ │ ├── cors_server_flowchart.png │ │ └── index.qmd │ ├── 04_simple_json_api │ │ └── screenshots │ │ │ ├── get-api-members.png │ │ │ ├── post-api-members.png │ │ │ ├── get-api-member-id.png │ │ │ ├── put-api-member-id.png │ │ │ ├── delete-api-member-id.png │ │ │ ├── get-api-member-id-not-found.png │ │ │ └── post-api-members-required-fields.png │ ├── _metadata.yml │ ├── 00_getting_started │ │ └── index.qmd │ ├── 08_datatables │ │ └── index.qmd │ ├── 13_parse_raw_json │ │ └── index.qmd │ ├── 01_hello_world │ │ └── index.qmd │ ├── 12_frontend_for_09_goals │ │ └── index.qmd │ ├── 07_dynamic_rendering │ │ └── index.qmd │ ├── 06_multi_router │ │ └── index.qmd │ ├── 02_static_files │ │ └── index.qmd │ └── 05_router │ │ └── index.qmd ├── index.qmd ├── _quarto.yml └── about.qmd ├── 08_datatables ├── .Rprofile ├── public │ └── styles.css ├── renv │ ├── .gitignore │ └── settings.json ├── docker-compose.yml ├── utils │ └── in_prod.R ├── controllers │ ├── home_get.R │ └── flights_get.R ├── Dockerfile ├── index.R ├── README.md └── store │ ├── create_card.R │ ├── create_href.R │ ├── page.R │ ├── home.R │ ├── datatable.R │ └── nav.R ├── 01_hello_world ├── .Rprofile ├── renv │ ├── .gitignore │ └── settings.json ├── README.md └── index.R ├── 02_static_files ├── .Rprofile ├── public │ ├── css │ │ └── styles.css │ ├── image.jpg │ ├── index.html │ ├── about.html │ └── index2.R ├── renv │ ├── .gitignore │ └── settings.json ├── index.R └── README.md ├── 03_basic_routing ├── .Rprofile ├── renv │ ├── .gitignore │ └── settings.json ├── get-home.png ├── post-home.png ├── put-user.png ├── delete-user.png ├── index.R └── README.md ├── 04_simple_json_api ├── .Rprofile ├── renv │ ├── .gitignore │ └── settings.json └── screenshots │ ├── get-api-members.png │ ├── post-api-members.png │ ├── get-api-member-id.png │ ├── put-api-member-id.png │ ├── delete-api-member-id.png │ ├── get-api-member-id-not-found.png │ └── post-api-members-required-fields.png ├── 06_multi_router ├── .Rprofile ├── renv │ ├── .gitignore │ └── settings.json ├── api │ ├── members.R │ ├── v1 │ │ ├── members │ │ │ ├── controllers.R │ │ │ ├── delete_member.R │ │ │ ├── get_all_members.R │ │ │ ├── create_new_member.R │ │ │ ├── update_member_info.R │ │ │ └── get_member_by_id.R │ │ └── members.R │ └── v2 │ │ ├── members │ │ ├── controllers.R │ │ ├── delete_member.R │ │ ├── get_all_members.R │ │ ├── create_new_member.R │ │ ├── update_member_info.R │ │ └── get_member_by_id.R │ │ └── members.R ├── server.R └── README.md ├── 11_csv_xlsx_upload ├── .Rprofile ├── renv │ ├── .gitignore │ └── settings.json ├── iris.xlsx ├── .gitignore ├── README.md └── example.R ├── 13_parse_raw_json ├── .Rprofile ├── .gitignore ├── renv │ ├── .gitignore │ └── settings.json ├── README.md └── server.R ├── 07_dynamic_rendering ├── .Rprofile ├── public │ └── styles.css ├── renv │ ├── .gitignore │ └── settings.json ├── controllers │ ├── template_path.R │ ├── contact.R │ ├── home_get.R │ ├── about_get.R │ ├── contact_get.R │ ├── validate_message.R │ ├── validate_email.R │ └── contact_post.R ├── templates │ ├── template_path.R │ ├── partials │ │ ├── footer.html │ │ └── header.html │ └── page.html ├── index.R ├── routers │ └── contact.R ├── store │ ├── home.R │ ├── about.R │ ├── create_card.R │ ├── nav.R │ └── contact.R └── README.md ├── 12_shiny_frontend_for_09_goals ├── .Rprofile ├── modules │ ├── mod.R │ ├── goals │ │ ├── mod.R │ │ ├── server.R │ │ ├── proxy.R │ │ ├── ui.R │ │ ├── dashboard_ui.R │ │ └── account_ui.R │ ├── auth │ │ ├── mod.R │ │ ├── login_ui.R │ │ ├── ui.R │ │ ├── server.R │ │ ├── login_server.R │ │ ├── signup_server.R │ │ └── proxy.R │ ├── ui.R │ └── server.R ├── renv │ ├── .gitignore │ └── settings.json ├── .gitignore ├── helpers │ ├── mod.R │ ├── env_vars.R │ └── operators.R ├── store │ ├── mod.R │ ├── center_modal.R │ ├── card.R │ ├── toast.R │ └── inputs.R ├── app.R ├── public │ └── script.js └── README.md └── README.md /.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .lintr 2 | -------------------------------------------------------------------------------- /docs/styles.css: -------------------------------------------------------------------------------- 1 | /* css styles */ 2 | -------------------------------------------------------------------------------- /05_router/box/.Renviron: -------------------------------------------------------------------------------- 1 | PORT = 3000 2 | -------------------------------------------------------------------------------- /09_goals/.Rprofile: -------------------------------------------------------------------------------- 1 | source("renv/activate.R") 2 | -------------------------------------------------------------------------------- /10_live_reloading/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /14_cors/.Rprofile: -------------------------------------------------------------------------------- 1 | source("renv/activate.R") 2 | -------------------------------------------------------------------------------- /15_chat/.Rprofile: -------------------------------------------------------------------------------- 1 | source("renv/activate.R") 2 | -------------------------------------------------------------------------------- /documentation/.gitignore: -------------------------------------------------------------------------------- 1 | /.quarto/ 2 | dev.R 3 | -------------------------------------------------------------------------------- /documentation/styles.css: -------------------------------------------------------------------------------- 1 | /* css styles */ 2 | -------------------------------------------------------------------------------- /05_router/box/.Rprofile: -------------------------------------------------------------------------------- 1 | source("renv/activate.R") 2 | -------------------------------------------------------------------------------- /05_router/r_pkg_structure/.Renviron: -------------------------------------------------------------------------------- 1 | PORT = 3000 2 | -------------------------------------------------------------------------------- /08_datatables/.Rprofile: -------------------------------------------------------------------------------- 1 | source("renv/activate.R") 2 | -------------------------------------------------------------------------------- /01_hello_world/.Rprofile: -------------------------------------------------------------------------------- 1 | source("renv/activate.R") 2 | -------------------------------------------------------------------------------- /02_static_files/.Rprofile: -------------------------------------------------------------------------------- 1 | source("renv/activate.R") 2 | -------------------------------------------------------------------------------- /03_basic_routing/.Rprofile: -------------------------------------------------------------------------------- 1 | source("renv/activate.R") 2 | -------------------------------------------------------------------------------- /04_simple_json_api/.Rprofile: -------------------------------------------------------------------------------- 1 | source("renv/activate.R") 2 | -------------------------------------------------------------------------------- /06_multi_router/.Rprofile: -------------------------------------------------------------------------------- 1 | source("renv/activate.R") 2 | -------------------------------------------------------------------------------- /10_live_reloading/.Rprofile: -------------------------------------------------------------------------------- 1 | source("renv/activate.R") 2 | -------------------------------------------------------------------------------- /11_csv_xlsx_upload/.Rprofile: -------------------------------------------------------------------------------- 1 | source("renv/activate.R") 2 | -------------------------------------------------------------------------------- /13_parse_raw_json/.Rprofile: -------------------------------------------------------------------------------- 1 | source("renv/activate.R") 2 | -------------------------------------------------------------------------------- /07_dynamic_rendering/.Rprofile: -------------------------------------------------------------------------------- 1 | source("renv/activate.R") 2 | -------------------------------------------------------------------------------- /05_router/r_pkg_structure/.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^renv$ 2 | ^renv\.lock$ 3 | -------------------------------------------------------------------------------- /05_router/r_pkg_structure/.Rprofile: -------------------------------------------------------------------------------- 1 | source("renv/activate.R") 2 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/.Rprofile: -------------------------------------------------------------------------------- 1 | source("renv/activate.R") 2 | -------------------------------------------------------------------------------- /15_chat/ui/__init__.R: -------------------------------------------------------------------------------- 1 | #' @export 2 | box::use( 3 | ./page[page], 4 | ) 5 | -------------------------------------------------------------------------------- /08_datatables/public/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Poppins", sans-serif; 3 | } 4 | -------------------------------------------------------------------------------- /07_dynamic_rendering/public/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Poppins", sans-serif; 3 | } 4 | -------------------------------------------------------------------------------- /02_static_files/public/css/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #333; 3 | color: #fff; 4 | } 5 | -------------------------------------------------------------------------------- /09_goals/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | package.json 4 | nodemon.json 5 | dev.R 6 | -------------------------------------------------------------------------------- /09_goals/renv/.gitignore: -------------------------------------------------------------------------------- 1 | library/ 2 | local/ 3 | cellar/ 4 | lock/ 5 | python/ 6 | sandbox/ 7 | staging/ 8 | -------------------------------------------------------------------------------- /14_cors/renv/.gitignore: -------------------------------------------------------------------------------- 1 | library/ 2 | local/ 3 | cellar/ 4 | lock/ 5 | python/ 6 | sandbox/ 7 | staging/ 8 | -------------------------------------------------------------------------------- /15_chat/renv/.gitignore: -------------------------------------------------------------------------------- 1 | library/ 2 | local/ 3 | cellar/ 4 | lock/ 5 | python/ 6 | sandbox/ 7 | staging/ 8 | -------------------------------------------------------------------------------- /05_router/box/renv/.gitignore: -------------------------------------------------------------------------------- 1 | library/ 2 | local/ 3 | cellar/ 4 | lock/ 5 | python/ 6 | sandbox/ 7 | staging/ 8 | -------------------------------------------------------------------------------- /08_datatables/renv/.gitignore: -------------------------------------------------------------------------------- 1 | library/ 2 | local/ 3 | cellar/ 4 | lock/ 5 | python/ 6 | sandbox/ 7 | staging/ 8 | -------------------------------------------------------------------------------- /01_hello_world/renv/.gitignore: -------------------------------------------------------------------------------- 1 | library/ 2 | local/ 3 | cellar/ 4 | lock/ 5 | python/ 6 | sandbox/ 7 | staging/ 8 | -------------------------------------------------------------------------------- /02_static_files/renv/.gitignore: -------------------------------------------------------------------------------- 1 | library/ 2 | local/ 3 | cellar/ 4 | lock/ 5 | python/ 6 | sandbox/ 7 | staging/ 8 | -------------------------------------------------------------------------------- /03_basic_routing/renv/.gitignore: -------------------------------------------------------------------------------- 1 | library/ 2 | local/ 3 | cellar/ 4 | lock/ 5 | python/ 6 | sandbox/ 7 | staging/ 8 | -------------------------------------------------------------------------------- /04_simple_json_api/renv/.gitignore: -------------------------------------------------------------------------------- 1 | library/ 2 | local/ 3 | cellar/ 4 | lock/ 5 | python/ 6 | sandbox/ 7 | staging/ 8 | -------------------------------------------------------------------------------- /06_multi_router/renv/.gitignore: -------------------------------------------------------------------------------- 1 | library/ 2 | local/ 3 | cellar/ 4 | lock/ 5 | python/ 6 | sandbox/ 7 | staging/ 8 | -------------------------------------------------------------------------------- /10_live_reloading/renv/.gitignore: -------------------------------------------------------------------------------- 1 | library/ 2 | local/ 3 | cellar/ 4 | lock/ 5 | python/ 6 | sandbox/ 7 | staging/ 8 | -------------------------------------------------------------------------------- /11_csv_xlsx_upload/renv/.gitignore: -------------------------------------------------------------------------------- 1 | library/ 2 | local/ 3 | cellar/ 4 | lock/ 5 | python/ 6 | sandbox/ 7 | staging/ 8 | -------------------------------------------------------------------------------- /13_parse_raw_json/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | nodemon.json 3 | package-lock.json 4 | package.json 5 | .Renviron 6 | -------------------------------------------------------------------------------- /13_parse_raw_json/renv/.gitignore: -------------------------------------------------------------------------------- 1 | library/ 2 | local/ 3 | cellar/ 4 | lock/ 5 | python/ 6 | sandbox/ 7 | staging/ 8 | -------------------------------------------------------------------------------- /15_chat/chat/__init__.R: -------------------------------------------------------------------------------- 1 | #' @export 2 | box::use( 3 | ./chat[ 4 | chat_get, 5 | chat_ws, 6 | ], 7 | ) 8 | -------------------------------------------------------------------------------- /documentation/profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/profile.jpg -------------------------------------------------------------------------------- /05_router/r_pkg_structure/server.R: -------------------------------------------------------------------------------- 1 | library(ambiorix) 2 | 3 | pkgload::load_all() 4 | 5 | app()$start(open = FALSE) 6 | -------------------------------------------------------------------------------- /07_dynamic_rendering/renv/.gitignore: -------------------------------------------------------------------------------- 1 | library/ 2 | local/ 3 | cellar/ 4 | lock/ 5 | python/ 6 | sandbox/ 7 | staging/ 8 | -------------------------------------------------------------------------------- /10_live_reloading/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "execMap": { 3 | "R": "Rscript" 4 | }, 5 | "ext": "R,html,css,js" 6 | } 7 | -------------------------------------------------------------------------------- /11_csv_xlsx_upload/iris.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/11_csv_xlsx_upload/iris.xlsx -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/modules/mod.R: -------------------------------------------------------------------------------- 1 | #' @export 2 | box::use( 3 | . / ui[ui], 4 | . / server[server], 5 | ) 6 | -------------------------------------------------------------------------------- /03_basic_routing/get-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/03_basic_routing/get-home.png -------------------------------------------------------------------------------- /03_basic_routing/post-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/03_basic_routing/post-home.png -------------------------------------------------------------------------------- /03_basic_routing/put-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/03_basic_routing/put-user.png -------------------------------------------------------------------------------- /05_router/r_pkg_structure/renv/.gitignore: -------------------------------------------------------------------------------- 1 | library/ 2 | local/ 3 | cellar/ 4 | lock/ 5 | python/ 6 | sandbox/ 7 | staging/ 8 | -------------------------------------------------------------------------------- /09_goals/.Renviron: -------------------------------------------------------------------------------- 1 | PORT = 5000 2 | JWT_SECRET = 312d6002-b53a-4045-84ed-7da926f569f9 3 | RENV_CONFIG_SANDBOX_ENABLED = FALSE 4 | -------------------------------------------------------------------------------- /11_csv_xlsx_upload/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | nodemon.json 3 | package-lock.json 4 | package.json 5 | .Renviron 6 | dev.R 7 | -------------------------------------------------------------------------------- /02_static_files/public/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/02_static_files/public/image.jpg -------------------------------------------------------------------------------- /03_basic_routing/delete-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/03_basic_routing/delete-user.png -------------------------------------------------------------------------------- /05_router/box/data/members.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/05_router/box/data/members.sqlite -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/renv/.gitignore: -------------------------------------------------------------------------------- 1 | library/ 2 | local/ 3 | cellar/ 4 | lock/ 5 | python/ 6 | sandbox/ 7 | staging/ 8 | -------------------------------------------------------------------------------- /06_multi_router/api/members.R: -------------------------------------------------------------------------------- 1 | #' @export 2 | box::use( 3 | . / v1 / members[v1 = router], 4 | . / v2 / members[v2 = router] 5 | ) 6 | -------------------------------------------------------------------------------- /07_dynamic_rendering/controllers/template_path.R: -------------------------------------------------------------------------------- 1 | #' @export 2 | box::use( 3 | .. / templates / template_path[template_path] 4 | ) 5 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | nodemon.json 3 | package-lock.json 4 | package.json 5 | .Renviron 6 | dev.R 7 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/helpers/mod.R: -------------------------------------------------------------------------------- 1 | #' @export 2 | box::use( 3 | . / operators[`%||%`], 4 | . / env_vars[get_base_url], 5 | ) 6 | -------------------------------------------------------------------------------- /docs/posts/03_basic_routing/get-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/posts/03_basic_routing/get-home.png -------------------------------------------------------------------------------- /docs/posts/03_basic_routing/post-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/posts/03_basic_routing/post-home.png -------------------------------------------------------------------------------- /docs/posts/03_basic_routing/put-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/posts/03_basic_routing/put-user.png -------------------------------------------------------------------------------- /05_router/box/data/get_db_path.R: -------------------------------------------------------------------------------- 1 | #' Get database path 2 | #' @export 3 | get_db_path <- \() { 4 | file.path(box::file(), "members.sqlite") 5 | } 6 | -------------------------------------------------------------------------------- /09_goals/models/goal_model.R: -------------------------------------------------------------------------------- 1 | #' Goal schema 2 | #' 3 | goal_schema <- data.frame( 4 | user_id = character(), # oid 5 | text = character() 6 | ) 7 | -------------------------------------------------------------------------------- /docs/posts/03_basic_routing/delete-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/posts/03_basic_routing/delete-user.png -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/modules/goals/mod.R: -------------------------------------------------------------------------------- 1 | #' @export 2 | box::use( 3 | . / ui[goals_ui = ui], 4 | . / server[goals_server = server], 5 | ) 6 | -------------------------------------------------------------------------------- /docs/posts/14_cors/cors_server_flowchart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/posts/14_cors/cors_server_flowchart.png -------------------------------------------------------------------------------- /docs/site_libs/bootstrap/bootstrap-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/site_libs/bootstrap/bootstrap-icons.woff -------------------------------------------------------------------------------- /05_router/box/routes/api/members/get_db_path.R: -------------------------------------------------------------------------------- 1 | #' Get path to database 2 | #' @export 3 | box::use( 4 | .. / .. / .. / data / get_db_path[get_db_path] 5 | ) 6 | -------------------------------------------------------------------------------- /04_simple_json_api/screenshots/get-api-members.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/04_simple_json_api/screenshots/get-api-members.png -------------------------------------------------------------------------------- /04_simple_json_api/screenshots/post-api-members.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/04_simple_json_api/screenshots/post-api-members.png -------------------------------------------------------------------------------- /documentation/posts/03_basic_routing/get-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/posts/03_basic_routing/get-home.png -------------------------------------------------------------------------------- /documentation/posts/03_basic_routing/post-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/posts/03_basic_routing/post-home.png -------------------------------------------------------------------------------- /documentation/posts/03_basic_routing/put-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/posts/03_basic_routing/put-user.png -------------------------------------------------------------------------------- /04_simple_json_api/screenshots/get-api-member-id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/04_simple_json_api/screenshots/get-api-member-id.png -------------------------------------------------------------------------------- /04_simple_json_api/screenshots/put-api-member-id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/04_simple_json_api/screenshots/put-api-member-id.png -------------------------------------------------------------------------------- /08_datatables/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | datatables: 3 | image: datatables 4 | ports: 5 | - "127.0.0.1:3001:3000" 6 | restart: unless-stopped 7 | -------------------------------------------------------------------------------- /documentation/posts/03_basic_routing/delete-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/posts/03_basic_routing/delete-user.png -------------------------------------------------------------------------------- /documentation/posts/14_cors/cors_server_flowchart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/posts/14_cors/cors_server_flowchart.png -------------------------------------------------------------------------------- /04_simple_json_api/screenshots/delete-api-member-id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/04_simple_json_api/screenshots/delete-api-member-id.png -------------------------------------------------------------------------------- /docs/posts/04_simple_json_api/screenshots/get-api-members.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/posts/04_simple_json_api/screenshots/get-api-members.png -------------------------------------------------------------------------------- /04_simple_json_api/screenshots/get-api-member-id-not-found.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/04_simple_json_api/screenshots/get-api-member-id-not-found.png -------------------------------------------------------------------------------- /docs/posts/04_simple_json_api/screenshots/get-api-member-id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/posts/04_simple_json_api/screenshots/get-api-member-id.png -------------------------------------------------------------------------------- /docs/posts/04_simple_json_api/screenshots/post-api-members.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/posts/04_simple_json_api/screenshots/post-api-members.png -------------------------------------------------------------------------------- /docs/posts/04_simple_json_api/screenshots/put-api-member-id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/posts/04_simple_json_api/screenshots/put-api-member-id.png -------------------------------------------------------------------------------- /07_dynamic_rendering/templates/template_path.R: -------------------------------------------------------------------------------- 1 | #' Template path 2 | #' 3 | #' Create a path to `templates/` 4 | #' @export 5 | template_path <- \(...) { 6 | file.path(box::file(), ...) 7 | } 8 | -------------------------------------------------------------------------------- /docs/posts/04_simple_json_api/screenshots/delete-api-member-id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/posts/04_simple_json_api/screenshots/delete-api-member-id.png -------------------------------------------------------------------------------- /04_simple_json_api/screenshots/post-api-members-required-fields.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/04_simple_json_api/screenshots/post-api-members-required-fields.png -------------------------------------------------------------------------------- /documentation/posts/04_simple_json_api/screenshots/get-api-members.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/posts/04_simple_json_api/screenshots/get-api-members.png -------------------------------------------------------------------------------- /documentation/posts/04_simple_json_api/screenshots/post-api-members.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/posts/04_simple_json_api/screenshots/post-api-members.png -------------------------------------------------------------------------------- /docs/posts/04_simple_json_api/screenshots/get-api-member-id-not-found.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/posts/04_simple_json_api/screenshots/get-api-member-id-not-found.png -------------------------------------------------------------------------------- /documentation/posts/04_simple_json_api/screenshots/get-api-member-id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/posts/04_simple_json_api/screenshots/get-api-member-id.png -------------------------------------------------------------------------------- /documentation/posts/04_simple_json_api/screenshots/put-api-member-id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/posts/04_simple_json_api/screenshots/put-api-member-id.png -------------------------------------------------------------------------------- /08_datatables/utils/in_prod.R: -------------------------------------------------------------------------------- 1 | #' Is app running in prod? 2 | #' 3 | #' @return Logical. 4 | #' @export 5 | in_prod <- \() { 6 | identical( 7 | Sys.getenv("APP_ENV"), 8 | "prod" 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /documentation/posts/04_simple_json_api/screenshots/delete-api-member-id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/posts/04_simple_json_api/screenshots/delete-api-member-id.png -------------------------------------------------------------------------------- /docs/posts/04_simple_json_api/screenshots/post-api-members-required-fields.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/posts/04_simple_json_api/screenshots/post-api-members-required-fields.png -------------------------------------------------------------------------------- /07_dynamic_rendering/templates/partials/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /documentation/posts/04_simple_json_api/screenshots/get-api-member-id-not-found.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/posts/04_simple_json_api/screenshots/get-api-member-id-not-found.png -------------------------------------------------------------------------------- /07_dynamic_rendering/controllers/contact.R: -------------------------------------------------------------------------------- 1 | #' @export 2 | box::use( 3 | . / contact_get[contact_get], 4 | . / contact_post[contact_post], 5 | . / validate_email[validate_email], 6 | . / validate_message[validate_message] 7 | ) 8 | -------------------------------------------------------------------------------- /documentation/posts/04_simple_json_api/screenshots/post-api-members-required-fields.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/posts/04_simple_json_api/screenshots/post-api-members-required-fields.png -------------------------------------------------------------------------------- /05_router/r_pkg_structure/R/get_all_members.R: -------------------------------------------------------------------------------- 1 | #' Get all members controller function 2 | #' 3 | #' @inheritParams handler 4 | #' @name views 5 | #' @keywords internal 6 | 7 | get_all_members <- \(req, res) { 8 | res$json(.GlobalEnv$members) 9 | } 10 | -------------------------------------------------------------------------------- /10_live_reloading/server.R: -------------------------------------------------------------------------------- 1 | library(ambiorix) 2 | 3 | app <- Ambiorix$new() 4 | 5 | app$get("/", \(req, res){ 6 | res$send("Using {ambiorix}!") 7 | }) 8 | 9 | app$get("/about", \(req, res){ 10 | res$text("About") 11 | }) 12 | 13 | app$start() 14 | -------------------------------------------------------------------------------- /08_datatables/controllers/home_get.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | .. / store / home[home] 3 | ) 4 | 5 | #' Home 6 | #' 7 | #' Handler for GET requests at "/". Renders the homepage. 8 | #' 9 | #' @export 10 | home_get <- \(req, res) { 11 | res$send(home()) 12 | } 13 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/store/mod.R: -------------------------------------------------------------------------------- 1 | #' @export 2 | box::use( 3 | . / inputs[ 4 | text_input, 5 | email_input, 6 | password_input, 7 | ], 8 | . / card[card], 9 | . / toast[toast_nofitication], 10 | . / center_modal[center_modal], 11 | ) 12 | -------------------------------------------------------------------------------- /08_datatables/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rocker/r-ver:4.3.3 2 | RUN apt-get update && apt-get install -y \ 3 | git-core \ 4 | libssl-dev 5 | WORKDIR /app 6 | COPY . . 7 | RUN rm -rdf renv/library 8 | RUN R -e "renv::restore()" 9 | EXPOSE 3000 10 | CMD ["Rscript", "index.R"] 11 | -------------------------------------------------------------------------------- /05_router/r_pkg_structure/R/app.R: -------------------------------------------------------------------------------- 1 | #' Core app 2 | #' 3 | #' @param port Port to run the app in. Defaults to [get_port()]. 4 | #' @export 5 | app <- \(port = get_port()) { 6 | app <- ambiorix::Ambiorix$new() 7 | app$listen(port = port) 8 | app$use(members_router()) 9 | app 10 | } 11 | -------------------------------------------------------------------------------- /06_multi_router/server.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | ambiorix[Ambiorix], 3 | . / api / members 4 | ) 5 | 6 | Ambiorix$ 7 | new()$ 8 | listen(port = 3000L)$ 9 | use(members$v1)$ # mount API v1 members' router 10 | use(members$v2)$ # mount API v2 members' router 11 | start(open = FALSE) 12 | -------------------------------------------------------------------------------- /documentation/posts/_metadata.yml: -------------------------------------------------------------------------------- 1 | # options specified here will apply to all posts in this folder 2 | 3 | # freeze computational output 4 | # (see https://quarto.org/docs/projects/code-execution.html#freeze) 5 | freeze: true 6 | 7 | # Enable banner style title blocks 8 | title-block-banner: true 9 | -------------------------------------------------------------------------------- /05_router/box/routes/api/members/controllers.R: -------------------------------------------------------------------------------- 1 | #' @export 2 | box::use( 3 | . / create_new_member[create_new_member], 4 | . / delete_member[delete_member], 5 | . / get_all_members[get_all_members], 6 | . / get_member_by_id[get_member_by_id], 7 | . / update_member_info[update_member_info] 8 | ) 9 | -------------------------------------------------------------------------------- /06_multi_router/api/v1/members/controllers.R: -------------------------------------------------------------------------------- 1 | #' @export 2 | box::use( 3 | . / create_new_member[create_new_member], 4 | . / delete_member[delete_member], 5 | . / get_all_members[get_all_members], 6 | . / get_member_by_id[get_member_by_id], 7 | . / update_member_info[update_member_info] 8 | ) 9 | -------------------------------------------------------------------------------- /06_multi_router/api/v2/members/controllers.R: -------------------------------------------------------------------------------- 1 | #' @export 2 | box::use( 3 | . / create_new_member[create_new_member], 4 | . / delete_member[delete_member], 5 | . / get_all_members[get_all_members], 6 | . / get_member_by_id[get_member_by_id], 7 | . / update_member_info[update_member_info] 8 | ) 9 | -------------------------------------------------------------------------------- /09_goals/helpers/mongo_query.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | . / to_json[to_json] 3 | ) 4 | 5 | #' Create a mongo query 6 | #' 7 | #' @param ... `key = value` pairs 8 | #' @examples 9 | #' mongo_query(a = 1, b = 2, c = 3, d = list(e = 5)) 10 | #' @export 11 | mongo_query <- \(...) list(...) |> to_json() 12 | -------------------------------------------------------------------------------- /09_goals/models/user_model.R: -------------------------------------------------------------------------------- 1 | #' User schema 2 | #' 3 | user_schema <- data.frame( 4 | name = character(), 5 | email = character(), 6 | password = character(), 7 | created_at = character(), # timestamp: UTC datetime format 8 | updated_at = character() # timestamp: UTC datetime format 9 | ) 10 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/modules/auth/mod.R: -------------------------------------------------------------------------------- 1 | #' @export 2 | box::use( 3 | . / ui[auth_ui = ui], 4 | . / proxy[ 5 | login, 6 | delete_account, 7 | req_error_handler, 8 | get_account_details, 9 | update_account_details, 10 | ], 11 | . / server[auth_server = server], 12 | ) 13 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/modules/auth/login_ui.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | shiny[NS], 3 | . / signup_ui[create_auth_page], 4 | ) 5 | 6 | #' Login UI module 7 | #' 8 | #' @param id String. Module id. 9 | #' @export 10 | ui <- \(id) { 11 | ns <- NS(id) 12 | create_auth_page(ns = ns, type = "login") 13 | } 14 | -------------------------------------------------------------------------------- /05_router/box/server.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | ambiorix[Ambiorix], 3 | . / helpers / get_port[get_port], 4 | . / middleware / logger[logger], 5 | . / routes / api / members 6 | ) 7 | 8 | Ambiorix$ 9 | new()$ 10 | listen(port = get_port())$ 11 | use(logger)$ 12 | use(members$router)$ 13 | start(open = FALSE) 14 | -------------------------------------------------------------------------------- /07_dynamic_rendering/templates/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | [! partials/header.html !] 6 | [% title %] 7 | 8 | 9 | [% content %] 10 | [! partials/footer.html !] 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /15_chat/chat/chat.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | .. / ui[page, ], 3 | ) 4 | 5 | #' @export 6 | chat_get <- function(req, res) { 7 | res$send(page()) 8 | } 9 | 10 | #' @export 11 | chat_ws <- function(msg, ws) { 12 | ambiorix::get_websocket_clients() |> 13 | lapply(\(c) { 14 | c$send("chat", msg) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /15_chat/index.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | ambiorix[Ambiorix], 3 | . / chat[ 4 | chat_get, 5 | chat_ws, 6 | ], 7 | ) 8 | 9 | PORT <- 3000 10 | 11 | app <- Ambiorix$new() 12 | 13 | app$static("public", "static") 14 | 15 | app$get("/", chat_get) 16 | app$receive("chat", chat_ws) 17 | 18 | app$start(port = PORT) 19 | -------------------------------------------------------------------------------- /08_datatables/index.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | ambiorix[Ambiorix], 3 | . / controllers / home_get[home_get], 4 | . / controllers / flights_get[flights_get] 5 | ) 6 | 7 | app <- Ambiorix$new(port = 3000L) 8 | app$static("public", "static") 9 | app$get("/", home_get) 10 | app$get("/data/flights", flights_get) 11 | app$start() 12 | -------------------------------------------------------------------------------- /05_router/box/middleware/logger.R: -------------------------------------------------------------------------------- 1 | #' Logger middleware 2 | #' 3 | #' @export 4 | logger <- \(req, res) { 5 | msg <- paste0( 6 | req$rook.url_scheme, 7 | "://", 8 | req$HTTP_HOST, 9 | req$PATH_INFO, 10 | " ", 11 | format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z") 12 | ) 13 | cat("\nLogger: ", msg, "\n\n") 14 | } 15 | -------------------------------------------------------------------------------- /09_goals/config/db.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | mongolite[mongo], 3 | jsonlite[toJSON] 4 | ) 5 | 6 | #' Goals db connection 7 | #' 8 | #' @export 9 | goals_conn <- mongo(collection = "goals", db = "mahr_tutorial") 10 | 11 | #' Users db connection 12 | #' 13 | #' @export 14 | users_conn <- mongo(collection = "users", db = "mahr_tutorial") 15 | -------------------------------------------------------------------------------- /02_static_files/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | My Website 7 | 8 | 9 | 10 |

My Website

11 | 12 | 13 | -------------------------------------------------------------------------------- /05_router/r_pkg_structure/R/get_port.R: -------------------------------------------------------------------------------- 1 | #' Get port from .Renviron file 2 | #' 3 | #' Looks for the env var `PORT` 4 | #' @inheritParams cli::cli_abort 5 | #' @return Character vector of length 1 6 | #' @examples 7 | #' \dontrun{ 8 | #' get_port() 9 | #' } 10 | get_port <- \(call = rlang::caller_env()) { 11 | check_port(call = call) 12 | Sys.getenv("PORT") 13 | } 14 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/app.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | shiny[shinyApp, addResourcePath], 3 | . / modules / mod[ui, server], 4 | ) 5 | 6 | addResourcePath( 7 | prefix = "static", 8 | directoryPath = box::file("public") 9 | ) 10 | 11 | shinyApp( 12 | ui = ui, 13 | server = server, 14 | options = list( 15 | port = 8000L, 16 | launch.browser = TRUE 17 | ) 18 | ) 19 | -------------------------------------------------------------------------------- /15_chat/public/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | min-height: 100vh; 3 | margin: 0 2rem; 4 | } 5 | 6 | .d-flex { 7 | display: flex; 8 | } 9 | 10 | .d-shrink { 11 | flex-shrink: 1; 12 | } 13 | 14 | .d-grow { 15 | flex-grow: 1; 16 | } 17 | 18 | .mb-1 { 19 | margin-bottom: 0.5rem; 20 | } 21 | 22 | .mr-1 { 23 | margin-right: 0.5rem; 24 | } 25 | 26 | .nes-balloon { 27 | min-width: 80%; 28 | } 29 | -------------------------------------------------------------------------------- /07_dynamic_rendering/index.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | ambiorix[Ambiorix], 3 | . / controllers / home_get[home_get], 4 | . / controllers / about_get[about_get], 5 | . / routers / contact[contact_router = router] 6 | ) 7 | 8 | Ambiorix$ 9 | new(port = 3000L)$ 10 | static("public", "static")$ 11 | get("/", home_get)$ 12 | get("/about", about_get)$ 13 | use(contact_router)$ 14 | start(open = TRUE) 15 | -------------------------------------------------------------------------------- /documentation/index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Documentation" 3 | listing: 4 | contents: posts 5 | fields: [title, subtitle, date, author] 6 | sort: "title asc" 7 | type: grid 8 | categories: true 9 | sort-ui: true 10 | filter-ui: false 11 | page-layout: full 12 | title-block-banner: true 13 | --- 14 | 15 | Welcome to the docs for the ✨[ambiorix-examples](https://github.com/ambiorix-web/ambiorix-examples)✨ 16 | -------------------------------------------------------------------------------- /05_router/box/helpers/get_port.R: -------------------------------------------------------------------------------- 1 | box::use(./check_port[check_port]) 2 | 3 | #' Get port from .Renviron file 4 | #' 5 | #' Looks for the env var `PORT` 6 | #' @inheritParams cli::cli_abort 7 | #' @return Character vector of length 1 8 | #' @examples 9 | #' \dontrun{ 10 | #' get_port() 11 | #' } 12 | #' @export 13 | get_port <- \(call = rlang::caller_env()) { 14 | check_port(call = call) 15 | Sys.getenv("PORT") 16 | } 17 | -------------------------------------------------------------------------------- /07_dynamic_rendering/controllers/home_get.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | . / template_path[template_path], 3 | .. / store / home[home] 4 | ) 5 | 6 | #' Home 7 | #' 8 | #' Handler for GET requests at "/". Renders the homepage. 9 | #' 10 | #' @export 11 | home_get <- \(req, res) { 12 | res$render( 13 | template_path("page.html"), 14 | list( 15 | title = "Home", 16 | content = home() 17 | ) 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /06_multi_router/api/v1/members/delete_member.R: -------------------------------------------------------------------------------- 1 | # don't forget to import any deps up here using box::use() 2 | 3 | #' Delete member controller function 4 | #' 5 | #' @inheritParams handler 6 | #' @name views 7 | #' @keywords internal 8 | #' @export 9 | delete_member <- \(req, res) { 10 | # 11 | response <- list(message = "Delete member (API v1)") 12 | res$set_status(200)$json(response) 13 | } 14 | -------------------------------------------------------------------------------- /06_multi_router/api/v2/members/delete_member.R: -------------------------------------------------------------------------------- 1 | # don't forget to import any deps up here using box::use() 2 | 3 | #' Delete member controller function 4 | #' 5 | #' @inheritParams handler 6 | #' @name views 7 | #' @keywords internal 8 | #' @export 9 | delete_member <- \(req, res) { 10 | # 11 | response <- list(message = "Delete member (API v2)") 12 | res$set_status(200)$json(response) 13 | } 14 | -------------------------------------------------------------------------------- /07_dynamic_rendering/routers/contact.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | ambiorix[Router], 3 | .. / controllers / contact[ 4 | contact_get, 5 | contact_post, 6 | validate_email, 7 | validate_message 8 | ] 9 | ) 10 | 11 | #' @export 12 | router <- Router$new("/contact") 13 | 14 | router$ 15 | get("/", contact_get)$ 16 | post("/", contact_post)$ 17 | post("/email", validate_email)$ 18 | post("/message", validate_message) 19 | -------------------------------------------------------------------------------- /07_dynamic_rendering/controllers/about_get.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | . / template_path[template_path], 3 | .. / store / about[about] 4 | ) 5 | 6 | #' About 7 | #' 8 | #' Handler for GET requests at "/about". Renders the about page. 9 | #' 10 | #' @export 11 | about_get <- \(req, res) { 12 | res$render( 13 | template_path("page.html"), 14 | list( 15 | title = "About", 16 | content = about() 17 | ) 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /documentation/_quarto.yml: -------------------------------------------------------------------------------- 1 | project: 2 | type: website 3 | output-dir: ../docs 4 | 5 | website: 6 | title: "ambiorix-examples" 7 | navbar: 8 | right: 9 | - about.qmd 10 | - icon: github 11 | href: https://github.com/ambiorix-web/ambiorix-examples 12 | - icon: twitter 13 | href: https://x.com/ambiorixweb 14 | format: 15 | html: 16 | theme: lux 17 | css: styles.css 18 | toc: true 19 | -------------------------------------------------------------------------------- /06_multi_router/api/v1/members/get_all_members.R: -------------------------------------------------------------------------------- 1 | # don't forget to import any deps up here using box::use() 2 | 3 | #' Get all members controller function 4 | #' 5 | #' @inheritParams handler 6 | #' @name views 7 | #' @keywords internal 8 | #' @export 9 | get_all_members <- \(req, res) { 10 | # 11 | response <- list(message = "Get all members (API v1)") 12 | res$set_status(200)$json(response) 13 | } 14 | -------------------------------------------------------------------------------- /06_multi_router/api/v2/members/get_all_members.R: -------------------------------------------------------------------------------- 1 | # don't forget to import any deps up here using box::use() 2 | 3 | #' Get all members controller function 4 | #' 5 | #' @inheritParams handler 6 | #' @name views 7 | #' @keywords internal 8 | #' @export 9 | get_all_members <- \(req, res) { 10 | # 11 | response <- list(message = "Get all members (API v2)") 12 | res$set_status(200)$json(response) 13 | } 14 | -------------------------------------------------------------------------------- /09_goals/server.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | ambiorix[Ambiorix], 3 | . / helpers / get_port[get_port], 4 | . / middleware / error_middleware[error_handler], 5 | . / routes / goal_routes[goal_router = router], 6 | . / routes / user_routes[user_router = router] 7 | ) 8 | 9 | app <- Ambiorix$new() 10 | 11 | app$error <- error_handler 12 | 13 | app$ 14 | use(goal_router)$ 15 | use(user_router)$ 16 | start(port = get_port(), open = FALSE) 17 | -------------------------------------------------------------------------------- /06_multi_router/api/v1/members/create_new_member.R: -------------------------------------------------------------------------------- 1 | # don't forget to import any deps up here using box::use() 2 | 3 | #' Create new member controller function 4 | #' 5 | #' @inheritParams handler 6 | #' @name views 7 | #' @keywords internal 8 | #' @export 9 | create_new_member <- \(req, res) { 10 | # 11 | response <- list(message = "Create new member (API v1)") 12 | res$set_status(200)$json(response) 13 | } 14 | -------------------------------------------------------------------------------- /06_multi_router/api/v2/members/create_new_member.R: -------------------------------------------------------------------------------- 1 | # don't forget to import any deps up here using box::use() 2 | 3 | #' Create new member controller function 4 | #' 5 | #' @inheritParams handler 6 | #' @name views 7 | #' @keywords internal 8 | #' @export 9 | create_new_member <- \(req, res) { 10 | # 11 | response <- list(message = "Create new member (API v2)") 12 | res$set_status(200)$json(response) 13 | } 14 | -------------------------------------------------------------------------------- /06_multi_router/api/v1/members/update_member_info.R: -------------------------------------------------------------------------------- 1 | # don't forget to import any deps up here using box::use() 2 | 3 | #' Update member controller function 4 | #' 5 | #' @inheritParams handler 6 | #' @name views 7 | #' @keywords internal 8 | #' @export 9 | update_member_info <- \(req, res) { 10 | # 11 | response <- list(message = "Update member info (API v1)") 12 | res$set_status(200)$json(response) 13 | } 14 | -------------------------------------------------------------------------------- /06_multi_router/api/v2/members/update_member_info.R: -------------------------------------------------------------------------------- 1 | # don't forget to import any deps up here using box::use() 2 | 3 | #' Update member controller function 4 | #' 5 | #' @inheritParams handler 6 | #' @name views 7 | #' @keywords internal 8 | #' @export 9 | update_member_info <- \(req, res) { 10 | # 11 | response <- list(message = "Update member info (API v2)") 12 | res$set_status(200)$json(response) 13 | } 14 | -------------------------------------------------------------------------------- /07_dynamic_rendering/controllers/contact_get.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | . / template_path[template_path], 3 | .. / store / contact[contact] 4 | ) 5 | 6 | #' Contact 7 | #' 8 | #' Handler for GET requests at "/contact". Renders the contact page. 9 | #' 10 | #' @export 11 | contact_get <- \(req, res) { 12 | res$render( 13 | template_path("page.html"), 14 | list( 15 | title = "Contact", 16 | content = contact() 17 | ) 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /05_router/box/models/members.R: -------------------------------------------------------------------------------- 1 | # box::use( 2 | # RSQLite[SQLite], 3 | # DBI[dbConnect, dbWriteTable, dbListTables] 4 | # ) 5 | 6 | # #' The members "model" 7 | # #' 8 | # members <- data.frame( 9 | # id = character(), 10 | # name = character(), 11 | # email = character(), 12 | # status = character() 13 | # ) 14 | 15 | # conn <- dbConnect(SQLite(), file.path("data", "members.sqlite")) 16 | 17 | # dbWriteTable(conn = conn, "members", members) 18 | -------------------------------------------------------------------------------- /06_multi_router/api/v1/members/get_member_by_id.R: -------------------------------------------------------------------------------- 1 | # don't forget to import any deps up here using box::use() 2 | 3 | #' Get member by id controller function 4 | #' 5 | #' @inheritParams handler 6 | #' @name views 7 | #' @keywords internal 8 | #' @export 9 | get_member_by_id <- \(req, res) { 10 | # 11 | response <- list(message = "Get a single member by id (API v1)") 12 | res$set_status(200)$json(response) 13 | } 14 | -------------------------------------------------------------------------------- /06_multi_router/api/v2/members/get_member_by_id.R: -------------------------------------------------------------------------------- 1 | # don't forget to import any deps up here using box::use() 2 | 3 | #' Get member by id controller function 4 | #' 5 | #' @inheritParams handler 6 | #' @name views 7 | #' @keywords internal 8 | #' @export 9 | get_member_by_id <- \(req, res) { 10 | # 11 | response <- list(message = "Get a single member by id (API v2)") 12 | res$set_status(200)$json(response) 13 | } 14 | -------------------------------------------------------------------------------- /01_hello_world/README.md: -------------------------------------------------------------------------------- 1 | ## Hello World example 2 | 3 | This app starts a server and listens on port 3000 for connections. 4 | 5 | The app responds with "Hello World" for requests to the root 6 | URL (/) or **route**. 7 | 8 | - Navigate to [localhost:3000/](http://localhost:3000/) on your browser to see that. 9 | - Navigate to [localhost:3000/about](http://localhost:3000/about) to see the about page. 10 | 11 | For every other path, it will respond with a **404 Not Found**. 12 | -------------------------------------------------------------------------------- /03_basic_routing/index.R: -------------------------------------------------------------------------------- 1 | library(ambiorix) 2 | PORT <- 3000 3 | 4 | app <- Ambiorix$new() 5 | 6 | app$get("/", \(req, res) { 7 | res$send("Hello World!") 8 | }) 9 | 10 | app$post("/", \(req, res) { 11 | res$send("Got a POST request") 12 | }) 13 | 14 | app$put("/user", \(req, res) { 15 | res$send("Got a PUT request at /user") 16 | }) 17 | 18 | app$delete("/user", \(req, res) { 19 | res$send("Got a DELETE request at /user") 20 | }) 21 | 22 | app$start(port = PORT) 23 | -------------------------------------------------------------------------------- /09_goals/helpers/get_jwt_secret.R: -------------------------------------------------------------------------------- 1 | #' Get jwt secret key 2 | #' 3 | #' Looks for the variable `JWT_SECRET` in your .Renviron 4 | #' @export 5 | get_jwt_secret <- \() { 6 | key <- Sys.getenv("JWT_SECRET") 7 | if (key == "") { 8 | stop( 9 | "JWT secret key not found.", 10 | " Set the `JWT_SECRET` variable in your .Renviron.", 11 | " You can use `uuid::UUIDgenerate()` to generate a secret key.", 12 | call. = FALSE 13 | ) 14 | } 15 | key 16 | } 17 | -------------------------------------------------------------------------------- /01_hello_world/index.R: -------------------------------------------------------------------------------- 1 | library(ambiorix) 2 | 3 | # port to listen on: 4 | PORT <- 3000 5 | 6 | # initialize a new ambiorix instance: 7 | app <- Ambiorix$new() 8 | 9 | # respond with “Hello World!” for requests to the root URL (/) or route: 10 | app$get("/", \(req, res) { 11 | res$send("Hello World!") 12 | }) 13 | 14 | # you can also use html tags: 15 | app$get("/about", \(req, res) { 16 | res$send("

About Us

") 17 | }) 18 | 19 | # start server: 20 | app$start(port = PORT) 21 | -------------------------------------------------------------------------------- /09_goals/routes/goal_routes.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | ambiorix[Router], 3 | .. / controllers / goal_controller[ 4 | get_goals, 5 | set_goal, 6 | update_goal, 7 | delete_goal 8 | ], 9 | .. / middleware / auth_middleware[protect] 10 | ) 11 | 12 | #' Goal router 13 | #' 14 | #' @export 15 | router <- Router$ 16 | new("/api/goals")$ 17 | use(protect)$ 18 | get("/", get_goals)$ 19 | post("/", set_goal)$ 20 | put("/:id", update_goal)$ 21 | delete("/:id", delete_goal) 22 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/store/center_modal.R: -------------------------------------------------------------------------------- 1 | #' Center modal dialog 2 | #' 3 | #' Vertically and horizontally centers [shiny::modalDialog] 4 | #' 5 | #' @param modal [shiny::modalDialog] 6 | #' @inherit email_verification_modal examples 7 | #' @return The centered modal dialog tags 8 | #' @export 9 | center_modal <- \(modal) { 10 | modal_tag_q <- modal |> htmltools::tagQuery() 11 | modal_tag_q$find(".modal-dialog")$addClass("modal-dialog-centered") 12 | modal_tag_q$allTags() 13 | } 14 | -------------------------------------------------------------------------------- /02_static_files/public/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | About 7 | 8 | 9 | 10 |

About Us

11 |
    12 |
  • We're concerned about you
  • 13 |
  • We are a global team
  • 14 |
  • Integrity is at the core of our values
  • 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /09_goals/middleware/error_middleware.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | cli[cli_alert_danger], 3 | .. / helpers / operators[`%||%`] 4 | ) 5 | 6 | #' Error handler 7 | #' 8 | #' @export 9 | error_handler <- \(req, res, error = NULL) { 10 | if (!is.null(error)) { 11 | error_msg <- conditionMessage(error) 12 | cli_alert_danger("Error: {error_msg}") 13 | } 14 | response <- list( 15 | code = 500L, 16 | msg = "A server error occurred!" 17 | ) 18 | 19 | res$set_status(500L)$json(response) 20 | } 21 | -------------------------------------------------------------------------------- /09_goals/renv/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "bioconductor.version": null, 3 | "external.libraries": [], 4 | "ignored.packages": [], 5 | "package.dependency.fields": [ 6 | "Imports", 7 | "Depends", 8 | "LinkingTo" 9 | ], 10 | "ppm.enabled": null, 11 | "ppm.ignored.urls": [], 12 | "r.version": null, 13 | "snapshot.type": "implicit", 14 | "use.cache": true, 15 | "vcs.ignore.cellar": true, 16 | "vcs.ignore.library": true, 17 | "vcs.ignore.local": true, 18 | "vcs.manage.ignores": true 19 | } 20 | -------------------------------------------------------------------------------- /14_cors/renv/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "bioconductor.version": null, 3 | "external.libraries": [], 4 | "ignored.packages": [], 5 | "package.dependency.fields": [ 6 | "Imports", 7 | "Depends", 8 | "LinkingTo" 9 | ], 10 | "ppm.enabled": null, 11 | "ppm.ignored.urls": [], 12 | "r.version": null, 13 | "snapshot.type": "implicit", 14 | "use.cache": true, 15 | "vcs.ignore.cellar": true, 16 | "vcs.ignore.library": true, 17 | "vcs.ignore.local": true, 18 | "vcs.manage.ignores": true 19 | } 20 | -------------------------------------------------------------------------------- /15_chat/renv/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "bioconductor.version": null, 3 | "external.libraries": [], 4 | "ignored.packages": [], 5 | "package.dependency.fields": [ 6 | "Imports", 7 | "Depends", 8 | "LinkingTo" 9 | ], 10 | "ppm.enabled": null, 11 | "ppm.ignored.urls": [], 12 | "r.version": null, 13 | "snapshot.type": "implicit", 14 | "use.cache": true, 15 | "vcs.ignore.cellar": true, 16 | "vcs.ignore.library": true, 17 | "vcs.ignore.local": true, 18 | "vcs.manage.ignores": true 19 | } 20 | -------------------------------------------------------------------------------- /01_hello_world/renv/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "bioconductor.version": null, 3 | "external.libraries": [], 4 | "ignored.packages": [], 5 | "package.dependency.fields": [ 6 | "Imports", 7 | "Depends", 8 | "LinkingTo" 9 | ], 10 | "ppm.enabled": null, 11 | "ppm.ignored.urls": [], 12 | "r.version": null, 13 | "snapshot.type": "implicit", 14 | "use.cache": true, 15 | "vcs.ignore.cellar": true, 16 | "vcs.ignore.library": true, 17 | "vcs.ignore.local": true, 18 | "vcs.manage.ignores": true 19 | } 20 | -------------------------------------------------------------------------------- /02_static_files/renv/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "bioconductor.version": null, 3 | "external.libraries": [], 4 | "ignored.packages": [], 5 | "package.dependency.fields": [ 6 | "Imports", 7 | "Depends", 8 | "LinkingTo" 9 | ], 10 | "ppm.enabled": null, 11 | "ppm.ignored.urls": [], 12 | "r.version": null, 13 | "snapshot.type": "implicit", 14 | "use.cache": true, 15 | "vcs.ignore.cellar": true, 16 | "vcs.ignore.library": true, 17 | "vcs.ignore.local": true, 18 | "vcs.manage.ignores": true 19 | } 20 | -------------------------------------------------------------------------------- /03_basic_routing/renv/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "bioconductor.version": null, 3 | "external.libraries": [], 4 | "ignored.packages": [], 5 | "package.dependency.fields": [ 6 | "Imports", 7 | "Depends", 8 | "LinkingTo" 9 | ], 10 | "ppm.enabled": null, 11 | "ppm.ignored.urls": [], 12 | "r.version": null, 13 | "snapshot.type": "implicit", 14 | "use.cache": true, 15 | "vcs.ignore.cellar": true, 16 | "vcs.ignore.library": true, 17 | "vcs.ignore.local": true, 18 | "vcs.manage.ignores": true 19 | } 20 | -------------------------------------------------------------------------------- /05_router/box/renv/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "bioconductor.version": null, 3 | "external.libraries": [], 4 | "ignored.packages": [], 5 | "package.dependency.fields": [ 6 | "Imports", 7 | "Depends", 8 | "LinkingTo" 9 | ], 10 | "ppm.enabled": null, 11 | "ppm.ignored.urls": [], 12 | "r.version": null, 13 | "snapshot.type": "implicit", 14 | "use.cache": true, 15 | "vcs.ignore.cellar": true, 16 | "vcs.ignore.library": true, 17 | "vcs.ignore.local": true, 18 | "vcs.manage.ignores": true 19 | } 20 | -------------------------------------------------------------------------------- /06_multi_router/renv/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "bioconductor.version": null, 3 | "external.libraries": [], 4 | "ignored.packages": [], 5 | "package.dependency.fields": [ 6 | "Imports", 7 | "Depends", 8 | "LinkingTo" 9 | ], 10 | "ppm.enabled": null, 11 | "ppm.ignored.urls": [], 12 | "r.version": null, 13 | "snapshot.type": "implicit", 14 | "use.cache": true, 15 | "vcs.ignore.cellar": true, 16 | "vcs.ignore.library": true, 17 | "vcs.ignore.local": true, 18 | "vcs.manage.ignores": true 19 | } 20 | -------------------------------------------------------------------------------- /08_datatables/renv/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "bioconductor.version": null, 3 | "external.libraries": [], 4 | "ignored.packages": [], 5 | "package.dependency.fields": [ 6 | "Imports", 7 | "Depends", 8 | "LinkingTo" 9 | ], 10 | "ppm.enabled": null, 11 | "ppm.ignored.urls": [], 12 | "r.version": null, 13 | "snapshot.type": "implicit", 14 | "use.cache": true, 15 | "vcs.ignore.cellar": true, 16 | "vcs.ignore.library": true, 17 | "vcs.ignore.local": true, 18 | "vcs.manage.ignores": true 19 | } 20 | -------------------------------------------------------------------------------- /09_goals/helpers/get_port.R: -------------------------------------------------------------------------------- 1 | #' Get port to run app on from .Renviron file 2 | #' 3 | #' Looks for the env var `PORT`, if not found defaults 4 | #' to `default`. 5 | #' @param default Default value for the port in case none is set in 6 | #' `.Renviron` 7 | #' @return Character vector of length 1 8 | #' @examples 9 | #' \dontrun{ 10 | #' get_port() 11 | #' } 12 | #' @export 13 | get_port <- \(default = 8000) { 14 | port <- Sys.getenv("PORT") 15 | port <- if (port == "") default else port 16 | as.integer(port) 17 | } 18 | -------------------------------------------------------------------------------- /04_simple_json_api/renv/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "bioconductor.version": null, 3 | "external.libraries": [], 4 | "ignored.packages": [], 5 | "package.dependency.fields": [ 6 | "Imports", 7 | "Depends", 8 | "LinkingTo" 9 | ], 10 | "ppm.enabled": null, 11 | "ppm.ignored.urls": [], 12 | "r.version": null, 13 | "snapshot.type": "implicit", 14 | "use.cache": true, 15 | "vcs.ignore.cellar": true, 16 | "vcs.ignore.library": true, 17 | "vcs.ignore.local": true, 18 | "vcs.manage.ignores": true 19 | } 20 | -------------------------------------------------------------------------------- /07_dynamic_rendering/renv/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "bioconductor.version": null, 3 | "external.libraries": [], 4 | "ignored.packages": [], 5 | "package.dependency.fields": [ 6 | "Imports", 7 | "Depends", 8 | "LinkingTo" 9 | ], 10 | "ppm.enabled": null, 11 | "ppm.ignored.urls": [], 12 | "r.version": null, 13 | "snapshot.type": "implicit", 14 | "use.cache": true, 15 | "vcs.ignore.cellar": true, 16 | "vcs.ignore.library": true, 17 | "vcs.ignore.local": true, 18 | "vcs.manage.ignores": true 19 | } 20 | -------------------------------------------------------------------------------- /10_live_reloading/renv/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "bioconductor.version": null, 3 | "external.libraries": [], 4 | "ignored.packages": [], 5 | "package.dependency.fields": [ 6 | "Imports", 7 | "Depends", 8 | "LinkingTo" 9 | ], 10 | "ppm.enabled": null, 11 | "ppm.ignored.urls": [], 12 | "r.version": null, 13 | "snapshot.type": "implicit", 14 | "use.cache": true, 15 | "vcs.ignore.cellar": true, 16 | "vcs.ignore.library": true, 17 | "vcs.ignore.local": true, 18 | "vcs.manage.ignores": true 19 | } 20 | -------------------------------------------------------------------------------- /11_csv_xlsx_upload/renv/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "bioconductor.version": null, 3 | "external.libraries": [], 4 | "ignored.packages": [], 5 | "package.dependency.fields": [ 6 | "Imports", 7 | "Depends", 8 | "LinkingTo" 9 | ], 10 | "ppm.enabled": null, 11 | "ppm.ignored.urls": [], 12 | "r.version": null, 13 | "snapshot.type": "implicit", 14 | "use.cache": true, 15 | "vcs.ignore.cellar": true, 16 | "vcs.ignore.library": true, 17 | "vcs.ignore.local": true, 18 | "vcs.manage.ignores": true 19 | } 20 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/store/card.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | htmltools[tags], 3 | ) 4 | 5 | #' Create a bootstrap card 6 | #' 7 | #' @param ... [htmltools::tags] Card content. 8 | #' @param class Character vector. Classes to apply to the card body. 9 | #' @return [htlmtools::tags$div()] 10 | #' @export 11 | card <- \(..., class = NULL) { 12 | class <- c("card-body", class) 13 | 14 | tags$div( 15 | class = "card border-2 border-light", 16 | tags$div( 17 | class = class, 18 | ... 19 | ) 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /13_parse_raw_json/renv/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "bioconductor.version": null, 3 | "external.libraries": [], 4 | "ignored.packages": [], 5 | "package.dependency.fields": [ 6 | "Imports", 7 | "Depends", 8 | "LinkingTo" 9 | ], 10 | "ppm.enabled": null, 11 | "ppm.ignored.urls": [], 12 | "r.version": null, 13 | "snapshot.type": "implicit", 14 | "use.cache": true, 15 | "vcs.ignore.cellar": true, 16 | "vcs.ignore.library": true, 17 | "vcs.ignore.local": true, 18 | "vcs.manage.ignores": true 19 | } 20 | -------------------------------------------------------------------------------- /05_router/box/routes/api/members.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | ambiorix[Router], 3 | . / members / controllers[...] 4 | ) 5 | 6 | #' @export 7 | router <- Router$new("/api/members") 8 | 9 | # get all members: 10 | router$get("/", get_all_members) 11 | 12 | # get a single member: 13 | router$get("/:id", get_member_by_id) 14 | 15 | # create a new member: 16 | router$post("/", create_new_member) 17 | 18 | # update member info: 19 | router$put("/:id", update_member_info) 20 | 21 | # delete member: 22 | router$delete("/:id", delete_member) 23 | -------------------------------------------------------------------------------- /05_router/r_pkg_structure/renv/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "bioconductor.version": null, 3 | "external.libraries": [], 4 | "ignored.packages": [], 5 | "package.dependency.fields": [ 6 | "Imports", 7 | "Depends", 8 | "LinkingTo" 9 | ], 10 | "ppm.enabled": null, 11 | "ppm.ignored.urls": [], 12 | "r.version": null, 13 | "snapshot.type": "implicit", 14 | "use.cache": true, 15 | "vcs.ignore.cellar": true, 16 | "vcs.ignore.library": true, 17 | "vcs.ignore.local": true, 18 | "vcs.manage.ignores": true 19 | } 20 | -------------------------------------------------------------------------------- /06_multi_router/api/v1/members.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | ambiorix[Router], 3 | . / members / controllers[...] 4 | ) 5 | 6 | #' @export 7 | router <- Router$new("/api/v1/members") 8 | 9 | # get all members: 10 | router$get("/", get_all_members) 11 | 12 | # get a single member: 13 | router$get("/:id", get_member_by_id) 14 | 15 | # create a new member: 16 | router$post("/", create_new_member) 17 | 18 | # update member info: 19 | router$put("/:id", update_member_info) 20 | 21 | # delete member: 22 | router$delete("/:id", delete_member) 23 | -------------------------------------------------------------------------------- /06_multi_router/api/v2/members.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | ambiorix[Router], 3 | . / members / controllers[...] 4 | ) 5 | 6 | #' @export 7 | router <- Router$new("/api/v2/members") 8 | 9 | # get all members: 10 | router$get("/", get_all_members) 11 | 12 | # get a single member: 13 | router$get("/:id", get_member_by_id) 14 | 15 | # create a new member: 16 | router$post("/", create_new_member) 17 | 18 | # update member info: 19 | router$put("/:id", update_member_info) 20 | 21 | # delete member: 22 | router$delete("/:id", delete_member) 23 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/renv/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "bioconductor.version": null, 3 | "external.libraries": [], 4 | "ignored.packages": [], 5 | "package.dependency.fields": [ 6 | "Imports", 7 | "Depends", 8 | "LinkingTo" 9 | ], 10 | "ppm.enabled": null, 11 | "ppm.ignored.urls": [], 12 | "r.version": null, 13 | "snapshot.type": "implicit", 14 | "use.cache": true, 15 | "vcs.ignore.cellar": true, 16 | "vcs.ignore.library": true, 17 | "vcs.ignore.local": true, 18 | "vcs.manage.ignores": true 19 | } 20 | -------------------------------------------------------------------------------- /05_router/box/routes/api/members/get_all_members.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | . / get_db_path[get_db_path], 3 | DBI[dbConnect, dbDisconnect, dbReadTable], 4 | RSQLite[SQLite] 5 | ) 6 | 7 | #' Get all members controller function 8 | #' 9 | #' @inheritParams handler 10 | #' @name views 11 | #' @keywords internal 12 | #' @export 13 | get_all_members <- \(req, res) { 14 | conn <- dbConnect(drv = SQLite(), get_db_path()) 15 | on.exit(dbDisconnect(conn)) 16 | 17 | members <- dbReadTable(conn = conn, name = "members") 18 | res$json(members) 19 | } 20 | -------------------------------------------------------------------------------- /09_goals/routes/user_routes.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | ambiorix[Router], 3 | .. / controllers / user_controller[ 4 | register_user, 5 | login_user, 6 | update_me, 7 | delete_me, 8 | get_me, 9 | ], 10 | .. / middleware / auth_middleware[protect] 11 | ) 12 | 13 | #' Users router 14 | #' 15 | #' @export 16 | router <- Router$ 17 | new("/api/users")$ 18 | use(protect)$ 19 | post("/", register_user)$ 20 | post("/login", login_user)$ 21 | delete("/me", delete_me)$ 22 | put("/me", update_me)$ 23 | get("/me", get_me) 24 | -------------------------------------------------------------------------------- /05_router/r_pkg_structure/R/members.R: -------------------------------------------------------------------------------- 1 | #' The members data.frame 2 | #' 3 | #' This is a global variable. This is NOT a good practice. It is here for 4 | #' example purposes ie. to provide a complete reprex. 5 | #' Please NEVER do this. You should use a database instead. 6 | members <- data.frame( 7 | id = as.character(1:3), 8 | name = c("John Doe", "Bob Williams", "Shannon Jackson"), 9 | email = c("john@gmail.com", "bob@gmail.com", "shannon@gmail.com"), 10 | status = c("active", "inactive", "active") 11 | ) 12 | 13 | .GlobalEnv$members <- members 14 | -------------------------------------------------------------------------------- /05_router/r_pkg_structure/R/members_router.R: -------------------------------------------------------------------------------- 1 | #' Members router 2 | #' 3 | #' @export 4 | members_router <- \() { 5 | router <- Router$new("/api/members") 6 | 7 | # get all members: 8 | router$get("/", get_all_members) 9 | 10 | # get a single member: 11 | router$get("/:id", get_member_by_id) 12 | 13 | # create a new member: 14 | router$post("/", create_new_member) 15 | 16 | # update member info: 17 | router$put("/:id", update_member_info) 18 | 19 | # delete member: 20 | router$delete("/:id", delete_member) 21 | 22 | router 23 | } 24 | -------------------------------------------------------------------------------- /11_csv_xlsx_upload/README.md: -------------------------------------------------------------------------------- 1 | # File upload 2 | 3 | This example shows how you can upload & parse csv and xlsx files in an ambiorix 4 | API. 5 | 6 | ## Run API 7 | 8 | 1. `cd` into the `11_csv_xlsx_upload/` dir: 9 | 10 | ```bash 11 | cd 11_csv_xlsx_upload/ 12 | ``` 13 | 14 | 1. Fire up R and restore package dependencies: 15 | 16 | ```r 17 | renv::restore() 18 | ``` 19 | 20 | 1. `server.R` is the entry point. Run this command in the terminal to start the 21 | API: 22 | 23 | ```bash 24 | Rscript server.R 25 | ``` 26 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/public/script.js: -------------------------------------------------------------------------------- 1 | function toggle_password(password_input_id, toggle_btn_id) { 2 | $(document).ready(function () { 3 | $("#" + toggle_btn_id).on("click", function () { 4 | let password_input = $("#" + password_input_id); 5 | let password_field_type = password_input.attr("type"); 6 | 7 | password_input.attr( 8 | "type", 9 | password_field_type === "password" ? "text" : "password" 10 | ); 11 | 12 | $(this).find("i").toggleClass("fa-eye fa-eye-slash"); 13 | }); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /07_dynamic_rendering/templates/partials/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /14_cors/README.md: -------------------------------------------------------------------------------- 1 | # CORS middleware 2 | 3 | This example shows how you can add a CORS middleware to your API. 4 | 5 | This is useful when making cross-origin requests from the browser (client-side). 6 | 7 | # Run API 8 | 9 | 1. `cd` into the `14_cors/` dir: 10 | 11 | ```bash 12 | cd 14_cors/ 13 | ``` 14 | 1. Fire up R and restore package dependencies: 15 | 16 | ```r 17 | renv::restore() 18 | ``` 19 | 1. `server.R` is the entry point. Run this command in the terminal to start the API: 20 | 21 | ```bash 22 | Rscript server.R 23 | ``` 24 | -------------------------------------------------------------------------------- /10_live_reloading/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "10_live_reloading", 3 | "version": "1.0.0", 4 | "description": "When building applications (whether backend or frontend), it gets tiring to manually stop & restart the app when you make changes to the source code.", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "nodemon --signal SIGTERM server.R" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "nodemon": "^3.0.3" 14 | }, 15 | "dependencies": { 16 | "braces": "^3.0.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /09_goals/helpers/generate_token.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | jose[jwt_claim, jwt_encode_hmac], 3 | lubridate[now], 4 | . / get_jwt_secret[get_jwt_secret] 5 | ) 6 | 7 | #' Generate JWT 8 | #' 9 | #' @param user_id User id 10 | #' @param expires_in How long before the token expires. Defaults to 1 month. 11 | #' @export 12 | generate_token <- \( 13 | user_id, 14 | expires_in = now("UTC") + months(1, abbreviate = FALSE) 15 | ) { 16 | jwt_encode_hmac( 17 | claim = jwt_claim( 18 | exp = expires_in, 19 | user_id = user_id 20 | ), 21 | secret = get_jwt_secret() 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /02_static_files/public/index2.R: -------------------------------------------------------------------------------- 1 | library(ambiorix) 2 | library(htmltools) 3 | PORT <- 3000 4 | 5 | app <- Ambiorix$new() 6 | 7 | app$static(path = "public", "static") 8 | 9 | app$get("/", \(req, res) { 10 | res$send( 11 | tags$div( 12 | tags$h1("Hello everyone"), 13 | tags$img(src = file.path("static", "image.jpg")) 14 | ) 15 | ) 16 | }) 17 | 18 | app$start(port = PORT) 19 | 20 | # run the app and navigate to these links in your browser: 21 | # - http://localhost:3000/static/index.html 22 | # - http://localhost:3000/static/about.html 23 | # - http://localhost:3000/static/index2.R 24 | -------------------------------------------------------------------------------- /documentation/posts/00_getting_started/index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "00: getting started" 3 | subtitle: "Installation" 4 | author: "Kennedy Mwavu" 5 | date: "2024-07-10" 6 | categories: [00_getting_started] 7 | --- 8 | 9 | ## Installation 10 | 11 | - Clone the repo: 12 | 13 | ```bash 14 | git clone git@github.com:ambiorix-web/ambiorix-examples.git 15 | ``` 16 | 17 | - `cd` into the root dir: 18 | 19 | ```bash 20 | cd ambiorix-examples 21 | ``` 22 | 23 | All the examples assume you're in this directory. 24 | 25 | ## Say hello 26 | 27 | You're now ready for a ✨[Hello, World!](../01_hello_world/index.qmd)✨ 28 | -------------------------------------------------------------------------------- /05_router/r_pkg_structure/DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: members 2 | Title: Create Members REST API 3 | Version: 0.0.0.9000 4 | Authors@R: 5 | person("your-first-name", "your-last-name", , "example@gmail.com", role = c("aut", "cre", "cph"), 6 | comment = c(ORCID = "your-orc-id")) 7 | Description: Generate a RESTful API using ambiorix via the standard R package structure. This is just an example. 8 | Encoding: UTF-8 9 | Roxygen: list(markdown = TRUE) 10 | RoxygenNote: 7.2.3 11 | Imports: 12 | ambiorix (>= 2.1.0), 13 | htmltools (>= 0.5.7), 14 | cli (>= 3.6.2), 15 | rlang (>= 1.1.2) 16 | Suggests: 17 | ambiorix.generator (>= 1.0.2) 18 | -------------------------------------------------------------------------------- /07_dynamic_rendering/store/home.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | htmltools[tags, tagList], 3 | . / nav[nav], 4 | . / create_card[create_card] 5 | ) 6 | 7 | #' The "Home" page 8 | #' 9 | #' @return An object of class `shiny.tag` 10 | #' @export 11 | home <- \() { 12 | tagList( 13 | nav(), 14 | tags$div( 15 | class = "container", 16 | create_card( 17 | title = "Hello World!", 18 | title_icon = tags$i(class = "bi bi-house-door"), 19 | title_class = "text-primary", 20 | class = "shadow-sm", 21 | tags$p("This is our homepage."), 22 | tags$p("ambiorix + htmx = 🚀") 23 | ) 24 | ) 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /05_router/r_pkg_structure/R/check_port.R: -------------------------------------------------------------------------------- 1 | #' Checks if the env var `PORT` is set 2 | #' 3 | #' @inheritParams cli::cli_abort 4 | #' @return `TRUE` (invisibly) if the env var `PORT` is found in `.Renviron`. 5 | #' Otherwise, throws an error. 6 | #' @examples 7 | #' \dontrun{ 8 | #' check_port() 9 | #' } 10 | check_port <- \(call = rlang::caller_env()) { 11 | if (Sys.getenv("PORT") == "") { 12 | cli::cli_abort( 13 | message = c( 14 | "x" = "Env var {.envvar PORT} not found.", 15 | "i" = "Please set it in your {.file .Renviron} and restart the R session." 16 | ), 17 | call = call 18 | ) 19 | } 20 | invisible(TRUE) 21 | } 22 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/modules/auth/ui.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | shiny[ 3 | NS, 4 | tabsetPanel, 5 | tabPanelBody, 6 | ], 7 | htmltools[tags], 8 | . / login_ui[login_ui = ui], 9 | . / signup_ui[signup_ui = ui], 10 | ) 11 | 12 | #' Auth module UI 13 | #' 14 | #' @param id String. Module id. 15 | #' @export 16 | ui <- \(id) { 17 | ns <- NS(id) 18 | 19 | tabsetPanel( 20 | id = ns("tabs"), 21 | type = "hidden", 22 | selected = "login", 23 | tabPanelBody( 24 | value = "login", 25 | login_ui(id = ns("login")) 26 | ), 27 | tabPanelBody( 28 | value = "signup", 29 | signup_ui(id = ns("signup")) 30 | ) 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /05_router/box/helpers/check_port.R: -------------------------------------------------------------------------------- 1 | box::use(cli[cli_abort]) 2 | #' Checks if the env var `PORT` is set 3 | #' 4 | #' @inheritParams cli::cli_abort 5 | #' @return `TRUE` (invisibly) if the env var `PORT` is found in `.Renviron`. 6 | #' Otherwise, throws an error. 7 | #' @examples 8 | #' \dontrun{ 9 | #' check_port() 10 | #' } 11 | #' @export 12 | check_port <- \(call = rlang::caller_env()) { 13 | if (Sys.getenv("PORT") == "") { 14 | cli_abort( 15 | message = c( 16 | "x" = "Env var {.envvar PORT} not found.", 17 | "i" = "Please set it in your {.file .Renviron} and restart the R session." 18 | ), 19 | call = call 20 | ) 21 | } 22 | invisible(TRUE) 23 | } 24 | -------------------------------------------------------------------------------- /02_static_files/index.R: -------------------------------------------------------------------------------- 1 | library(ambiorix) 2 | library(htmltools) 3 | PORT <- 3000 4 | 5 | app <- Ambiorix$new() 6 | 7 | # make the "public/" folder static, and accessible as "static/": 8 | app$static(path = "public", "static") 9 | # any files/folders in "public/" can now be accessed via your browser 10 | 11 | app$get("/", \(req, res) { 12 | res$send( 13 | tags$div( 14 | tags$h1("Hello everyone"), 15 | tags$img(src = file.path("static", "image.jpg")) 16 | ) 17 | ) 18 | }) 19 | 20 | app$start(port = PORT) 21 | 22 | # run the app and navigate to these links in your browser: 23 | # - http://localhost:3000/static/index.html 24 | # - http://localhost:3000/static/about.html 25 | # - http://localhost:3000/static/index2.R 26 | -------------------------------------------------------------------------------- /09_goals/helpers/to_json.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | jsonlite[toJSON] 3 | ) 4 | 5 | #' Convert R objects to JSON with some custom defaults 6 | #' 7 | #' @inheritParams jsonlite::toJSON 8 | #' @examples 9 | #' query <- list( 10 | #' name = "Larry", 11 | #' age = 32, 12 | #' gpa = 2.8, 13 | #' fullTime = FALSE, 14 | #' registerDate = lubridate::now("UTC"), 15 | #' courses = c("Biology", "Chemistry", "Calculus"), 16 | #' address = list( 17 | #' street = "123 Fake St.", 18 | #' city = "Bikini Bottom", 19 | #' zip = 12345 20 | #' ) 21 | #' ) |> 22 | #' to_json() 23 | #' 24 | #' query 25 | #' @export 26 | to_json <- \(x, pretty = TRUE, auto_unbox = TRUE, ...) { 27 | toJSON(x = x, pretty = TRUE, auto_unbox = TRUE, ...) 28 | } 29 | -------------------------------------------------------------------------------- /05_router/r_pkg_structure/R/get_member_by_id.R: -------------------------------------------------------------------------------- 1 | #' Get member by id controller function 2 | #' 3 | #' @inheritParams handler 4 | #' @name views 5 | #' @keywords internal 6 | get_member_by_id <- \(req, res) { 7 | # get the supplied id: 8 | member_id <- req$params$id 9 | 10 | members <- .GlobalEnv$members 11 | # filter member with that id: 12 | found <- members |> dplyr::filter(id == member_id) 13 | 14 | # if a member with that id was found, return the member: 15 | if (nrow(found) > 0) { 16 | return(res$json(found)) 17 | } 18 | 19 | # otherwise, change response status to 400 (Bad Request) 20 | # and provide a message: 21 | msg <- list(msg = sprintf("No member with the id of %s", member_id)) 22 | res$set_status(400L)$json(msg) 23 | } 24 | -------------------------------------------------------------------------------- /09_goals/helpers/operators.R: -------------------------------------------------------------------------------- 1 | #' Coalescing operator to specify a default value 2 | #' 3 | #' This operator is used to specify a default value for a variable if the 4 | #' original value is \code{NULL}. 5 | #' 6 | #' @param x a variable to check for \code{NULL} 7 | #' @param y the default value to return if \code{x} is \code{NULL} 8 | #' @return the first non-\code{NULL} value 9 | #' @name op-null-default 10 | #' @examples 11 | #' my_var <- NULL 12 | #' default_value <- "hello" 13 | #' result <- my_var %||% default_value 14 | #' result # "hello" 15 | #' 16 | #' my_var <- "world" 17 | #' default_value <- "hello" 18 | #' result <- my_var %||% default_value 19 | #' result # "world" 20 | #' 21 | #' @export 22 | "%||%" <- \(x, y) { 23 | if (is.null(x)) y else x 24 | } 25 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/helpers/env_vars.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | cli[cli_abort], 3 | rlang[caller_env], 4 | ) 5 | 6 | #' Get the backend's base url 7 | #' 8 | #' Checks for the env var `BASE_URL` in your .Renviron 9 | #' @param call [rlang::caller_env()] Environment of the calling function. 10 | #' @return String. 11 | #' @export 12 | get_base_url <- \(call = caller_env()) { 13 | base_url <- Sys.getenv("BASE_URL") 14 | 15 | not_found <- identical(base_url, "") 16 | if (not_found) { 17 | cli_abort( 18 | message = c( 19 | "x" = "env var {.envvar BASE_URL} not found in the project's {.file .Renviron}", 20 | "i" = "Did you forget to set it and restart your R session?" 21 | ) 22 | ) 23 | } 24 | 25 | base_url 26 | } 27 | -------------------------------------------------------------------------------- /05_router/box/helpers/operators.R: -------------------------------------------------------------------------------- 1 | #' Coalescing operator to specify a default value 2 | #' 3 | #' This operator is used to specify a default value for a variable if the 4 | #' original value is \code{NULL}. 5 | #' 6 | #' @param x a variable to check for \code{NULL} 7 | #' @param y the default value to return if \code{x} is \code{NULL} 8 | #' @return the first non-\code{NULL} value 9 | #' @name op-null-default 10 | #' @export 11 | #' @examples 12 | #' my_var <- NULL 13 | #' default_value <- "hello" 14 | #' result <- my_var %||% default_value 15 | #' result # "hello" 16 | #' 17 | #' my_var <- "world" 18 | #' default_value <- "hello" 19 | #' result <- my_var %||% default_value 20 | #' result # "world" 21 | #' 22 | "%||%" <- function(x, y) { 23 | if (is.null(x)) y else x 24 | } 25 | -------------------------------------------------------------------------------- /05_router/r_pkg_structure/R/operators.R: -------------------------------------------------------------------------------- 1 | #' Coalescing operator to specify a default value 2 | #' 3 | #' This operator is used to specify a default value for a variable if the 4 | #' original value is \code{NULL}. 5 | #' 6 | #' @param x a variable to check for \code{NULL} 7 | #' @param y the default value to return if \code{x} is \code{NULL} 8 | #' @return the first non-\code{NULL} value 9 | #' @name op-null-default 10 | #' @export 11 | #' @examples 12 | #' my_var <- NULL 13 | #' default_value <- "hello" 14 | #' result <- my_var %||% default_value 15 | #' result # "hello" 16 | #' 17 | #' my_var <- "world" 18 | #' default_value <- "hello" 19 | #' result <- my_var %||% default_value 20 | #' result # "world" 21 | #' 22 | "%||%" <- function(x, y) { 23 | if (is.null(x)) y else x 24 | } 25 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/helpers/operators.R: -------------------------------------------------------------------------------- 1 | #' Coalescing operator to specify a default value 2 | #' 3 | #' This operator is used to specify a default value for a variable if the 4 | #' original value is \code{NULL}. 5 | #' 6 | #' @param x a variable to check for \code{NULL} 7 | #' @param y the default value to return if \code{x} is \code{NULL} 8 | #' @return the first non-\code{NULL} value 9 | #' @name op-null-default 10 | #' @examples 11 | #' my_var <- NULL 12 | #' default_value <- "hello" 13 | #' result <- my_var %||% default_value 14 | #' result # "hello" 15 | #' 16 | #' my_var <- "world" 17 | #' default_value <- "hello" 18 | #' result <- my_var %||% default_value 19 | #' result # "world" 20 | #' 21 | #' @export 22 | "%||%" <- \(x, y) { 23 | if (is.null(x)) y else x 24 | } 25 | -------------------------------------------------------------------------------- /11_csv_xlsx_upload/example.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | curl[form_file], 3 | httr2[ 4 | request, 5 | req_perform, 6 | req_url_path, 7 | last_response, 8 | resp_body_json, 9 | req_body_multipart, 10 | ] 11 | ) 12 | 13 | base_url <- "http://127.0.0.1:3000" 14 | file <- form_file( 15 | path = "iris.csv", 16 | type = "text/csv", 17 | name = "iris.csv" 18 | ) 19 | 20 | req <- request(base_url = base_url) |> 21 | req_url_path("/csv") |> 22 | req_body_multipart(file = file) 23 | 24 | # use `tryCatch()` in case an error occurs while performing the request: 25 | tryCatch( 26 | expr = req |> 27 | req_perform() |> 28 | resp_body_json(), 29 | error = \(e) { 30 | print("An error occurred!") 31 | error <- last_response() |> resp_body_json() 32 | print(error) 33 | } 34 | ) 35 | -------------------------------------------------------------------------------- /docs/listings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "listing": "/index.html", 4 | "items": [ 5 | "/posts/00_getting_started/index.html", 6 | "/posts/01_hello_world/index.html", 7 | "/posts/02_static_files/index.html", 8 | "/posts/03_basic_routing/index.html", 9 | "/posts/04_simple_json_api/index.html", 10 | "/posts/05_router/index.html", 11 | "/posts/06_multi_router/index.html", 12 | "/posts/07_dynamic_rendering/index.html", 13 | "/posts/08_datatables/index.html", 14 | "/posts/09_goals/index.html", 15 | "/posts/10_live_reloading/index.html", 16 | "/posts/11_csv_xlsx_upload/index.html", 17 | "/posts/12_frontend_for_09_goals/index.html", 18 | "/posts/13_parse_raw_json/index.html", 19 | "/posts/14_cors/index.html" 20 | ] 21 | } 22 | ] -------------------------------------------------------------------------------- /09_goals/helpers/truthiness.R: -------------------------------------------------------------------------------- 1 | #' Is a value falsy? 2 | #' 3 | #' @description 4 | #' A falsy value is either: 5 | #' * `NULL` 6 | #' * An empty string "" 7 | #' * Has a length of 0 8 | #' A truthy value is the opposite of that. 9 | #' Please note that this is not a check for whether a value is `TRUE` or 10 | #' `FALSE`. 11 | #' @param x Value to check. 12 | #' @examples 13 | #' is_falsy("") # TRUE 14 | #' is_falsy("m") # FALSE 15 | #' is_falsy(NULL) # TRUE 16 | #' is_falsy(character(0L)) # TRUE 17 | #' 18 | #' is_truthy("") # FALSE 19 | #' is_truthy("m") # TRUE 20 | #' is_truthy(NULL) # FALSE 21 | #' is_truthy(character(0L)) # FALSE 22 | #' @return Logical. 23 | #' @export 24 | is_falsy <- \(x) { 25 | is.null(x) || identical(x, "") || length(x) == 0L 26 | } 27 | 28 | #' Is a value truthy? 29 | #' 30 | #' @rdname is_falsy 31 | #' @export 32 | is_truthy <- Negate(is_falsy) 33 | -------------------------------------------------------------------------------- /07_dynamic_rendering/store/about.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | htmltools[tags, tagList], 3 | . / nav[nav], 4 | . / create_card[create_card] 5 | ) 6 | 7 | #' The "About" page 8 | #' 9 | #' @return An object of class `shiny.tag` 10 | #' @export 11 | about <- \() { 12 | tagList( 13 | nav(active = "About"), 14 | tags$div( 15 | class = "container", 16 | create_card( 17 | title = "About Us", 18 | title_icon = tags$i(class = "bi bi-info-circle"), 19 | title_class = "text-primary", 20 | class = "shadow-sm", 21 | tags$p("Here are a few things you might want to know:"), 22 | tags$ul( 23 | tags$li("Ambiorix is an unopinionated framework"), 24 | tags$li("It doesn't care about what you decide to use for your frontend"), 25 | tags$li("Ambiorix works great with htmx, btw.") 26 | ) 27 | ) 28 | ) 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/modules/ui.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | htmltools[tags], 3 | bslib[page, bs_theme], 4 | shinytoastr[useToastr], 5 | cookies[add_cookie_handlers], 6 | shiny[tabsetPanel, tabPanelBody], 7 | shinyjs[useShinyjs], 8 | . / auth / mod[auth_ui], 9 | . / goals / mod[goals_ui], 10 | ) 11 | 12 | #' App UI 13 | #' 14 | #' @export 15 | ui <- page( 16 | title = "Goals", 17 | theme = bs_theme(version = 5, preset = "zephyr"), 18 | lang = "en", 19 | tags$head( 20 | tags$script(src = "static/script.js") 21 | ), 22 | useShinyjs(), 23 | useToastr(), 24 | tabsetPanel( 25 | id = "pages", 26 | type = "hidden", 27 | selected = "auth", 28 | tabPanelBody( 29 | value = "auth", 30 | auth_ui(id = "auth") 31 | ), 32 | tabPanelBody( 33 | value = "goals", 34 | goals_ui(id = "goals") 35 | ) 36 | ) 37 | ) |> 38 | add_cookie_handlers() 39 | -------------------------------------------------------------------------------- /05_router/r_pkg_structure/README.md: -------------------------------------------------------------------------------- 1 | ## Using standard R package structure with ambiorix 2 | 3 | Run the file [index.R](./index.R) to fire up the app. 4 | 5 | > [!CAUTION] 6 | > - In this example, I have commited the [.Renviron](./.Renviron) file. **NEVER** do that! This is just for example purposes and to kind of create a full reprex. 7 | > - I have also used and manipulated a global env var (`.GlobalEnv$members`). Please **NEVER** do this. You should use a database instead. 8 | 9 | Once you run the app, you'll be able to send requests via these endpoints: 10 | - `GET`: `http://localhost:3000/api/members` : Gets all members 11 | - `GET`: `http://localhost:3000/api/members/:id` : Get a single member by id 12 | - `POST`: `http://localhost:3000/api/members` : Create a new member 13 | - `PUT`: `http://localhost:3000/api/members/:id` : Update member info 14 | - `DELETE`: `http://localhost:3000/api/members/:id` : Delete a member 15 | -------------------------------------------------------------------------------- /05_router/r_pkg_structure/R/delete_member.R: -------------------------------------------------------------------------------- 1 | #' Delete member controller function 2 | #' 3 | #' @inheritParams handler 4 | #' @name views 5 | #' @keywords internal 6 | delete_member <- \(req, res) { 7 | # get the supplied id: 8 | member_id <- req$params$id 9 | 10 | members <- .GlobalEnv$members 11 | # filter member with that id: 12 | found <- members |> dplyr::filter(id == member_id) 13 | 14 | # if a member with that id is NOT found, change response status 15 | # and provide a message: 16 | if (nrow(found) == 0) { 17 | msg <- list(msg = sprintf("No member with the id of %s", member_id)) 18 | return(res$set_status(400L)$json(msg)) 19 | } 20 | 21 | # otherwise, proceed to delete member: 22 | .GlobalEnv$members <- members |> dplyr::filter(id != member_id) 23 | 24 | response <- list( 25 | msg = "Member deleted successfully", 26 | members = .GlobalEnv$members 27 | ) 28 | res$json(response) 29 | } 30 | -------------------------------------------------------------------------------- /13_parse_raw_json/README.md: -------------------------------------------------------------------------------- 1 | # Parse raw JSON 2 | 3 | Say you have an API endpoint that expects raw JSON data. How do you parse that? 4 | 5 | This example answers that and is in a way related to the example on [csv & xlsx upload](../11_csv_xlsx_upload). 6 | 7 | This is a simple example revolving around how you can select columns and filter 8 | rows in the `iris` dataset when the request body is something like this: 9 | 10 | ```json 11 | { 12 | "cols": ["Sepal.Length", "Petal.Width", "Species"], 13 | "species": ["virginica", "setosa"] 14 | } 15 | ``` 16 | 17 | # Run API 18 | 19 | 1. `cd` into the `13_parse_raw_json/` dir: 20 | 21 | ```bash 22 | cd 13_parse_raw_json/ 23 | ``` 24 | 1. Fire up R and restore package dependencies: 25 | 26 | ```r 27 | renv::restore() 28 | ``` 29 | 1. `server.R` is the entry point. Run this command in the terminal to start the API: 30 | 31 | ```bash 32 | Rscript server.R 33 | ``` 34 | -------------------------------------------------------------------------------- /documentation/posts/08_datatables/index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "08: datatables" 3 | subtitle: "R users love tables!" 4 | author: "Kennedy Mwavu" 5 | date: "2024-07-10" 6 | categories: [08_datatables] 7 | --- 8 | 9 | ## Run app 10 | 11 | 1. `cd` into the `08_datatables/` dir: 12 | 13 | ```bash 14 | cd 08_datatables/ 15 | ``` 16 | 17 | 1. Fire up R: 18 | 19 | ```bash 20 | R 21 | ``` 22 | 23 | 1. Restore package dependencies: 24 | 25 | ```r 26 | renv::restore() 27 | ``` 28 | 29 | Once done, exit R. 30 | 1. `index.R` is the entry point. To start the app, run this on the terminal: 31 | 32 | ```bash 33 | Rscript index.R 34 | ``` 35 | 36 | ## Explanation 37 | 38 | This app starts a server and listens on port 3000 for connections. 39 | 40 | At this point, I'm not sure there's anything much to say :) 41 | 42 | ## Goals 43 | 44 | Alright, time to build a ✨[CRUD application backend](../09_goals/index.qmd)✨. 45 | -------------------------------------------------------------------------------- /08_datatables/README.md: -------------------------------------------------------------------------------- 1 | ## datatables 2 | 3 | how to use [datatables](https://datatables.net/) in ambiorix: 4 | 5 | - rendering 6 | - pagination 7 | 8 | ## deployment 9 | 10 | ### `.Renviron` 11 | 12 | set these env vars when deploying: 13 | 14 | ``` 15 | APP_ENV = prod 16 | APP_BASE_PATH = /datatables 17 | ``` 18 | 19 | setting the `APP_BASE_PATH` variable is only important if you're deploying 20 | the app at a sub-path. 21 | 22 | for example, if the app is deployed at `https://try.ambiorix.dev/datatables`, 23 | the env var `APP_BASE_PATH` should be set to `/datatables`. 24 | 25 | ### Docker 26 | 27 | - build docker image: 28 | 29 | ``` 30 | sudo docker build -t datatables . 31 | ``` 32 | 33 | - start services: 34 | 35 | ``` 36 | docker compose up -d --remove-orphans 37 | ``` 38 | 39 | The app is now accessible on the host machine at **port 3001**. 40 | 41 | - stop services in this context: 42 | 43 | ``` 44 | sudo docker compose down 45 | ``` 46 | -------------------------------------------------------------------------------- /08_datatables/store/create_card.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | htmltools[tags] 3 | ) 4 | 5 | #' Create a custom bootstrap card 6 | #' 7 | #' @param title Card title 8 | #' @param title_icon Icon to place before the title. Can be a `shiny::tags$i()`. 9 | #' @param title_class String. Additional bootstrap classes for the title. 10 | #' @param class Additional classes to apply to the card container 11 | #' @param ... Card content 12 | #' @return [shiny::tags$div] 13 | #' @export 14 | create_card <- \( 15 | ..., 16 | title = NULL, 17 | title_icon = NULL, 18 | title_class = NULL, 19 | class = NULL 20 | ) { 21 | tags$div( 22 | class = paste("card p-1 p-md-4 border-0", class), 23 | tags$div( 24 | class = "card-body", 25 | if (!is.null(title) || !is.null(title_icon)) { 26 | tags$h3( 27 | class = paste("card-title fw-semibold fs-4 mb-3", title_class), 28 | title_icon, 29 | title 30 | ) 31 | }, 32 | ... 33 | ) 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /07_dynamic_rendering/store/create_card.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | htmltools[tags] 3 | ) 4 | 5 | #' Create a custom bootstrap card 6 | #' 7 | #' @param title Card title 8 | #' @param title_icon Icon to place before the title. Can be a `shiny::tags$i()`. 9 | #' @param title_class String. Additional bootstrap classes for the title. 10 | #' @param class Additional classes to apply to the card container 11 | #' @param ... Card content 12 | #' @return [shiny::tags$div] 13 | #' @export 14 | create_card <- \( 15 | ..., 16 | title = NULL, 17 | title_icon = NULL, 18 | title_class = NULL, 19 | class = NULL 20 | ) { 21 | tags$div( 22 | class = paste("card p-1 p-md-4 border-0", class), 23 | tags$div( 24 | class = "card-body", 25 | if (!is.null(title) || !is.null(title_icon)) { 26 | tags$h3( 27 | class = paste("card-title fw-semibold fs-4 mb-3", title_class), 28 | title_icon, 29 | title 30 | ) 31 | }, 32 | ... 33 | ) 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /07_dynamic_rendering/controllers/validate_message.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | ambiorix[parse_multipart], 3 | htmltools[tags], 4 | shiny[isTruthy], 5 | .. / store / contact[text_area_input] 6 | ) 7 | 8 | #' Validate message sent by user 9 | #' 10 | #' Handler for POST requests at "/contact/message". 11 | #' 12 | #' @return An object of class `shiny.tag` 13 | #' @export 14 | validate_message <- \(req, res) { 15 | body <- parse_multipart(req) 16 | text <- body$msg 17 | html <- check_valid_message(text) 18 | res$send(html) 19 | } 20 | 21 | #' Check if message sent by user is valid 22 | #' 23 | #' @return An object of class `shiny.tag` 24 | #' @export 25 | check_valid_message <- \(message) { 26 | is_valid <- isTruthy(message) 27 | 28 | text_area_input( 29 | value = message, 30 | input_class = if (is_valid) "is-valid" else "is-invalid", 31 | if (!is_valid) { 32 | tags$div( 33 | class = "invalid-feedback", 34 | "Please enter a message" 35 | ) 36 | } 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /09_goals/helpers/insert.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | uuid[UUIDgenerate], 3 | . / mongo_query[mongo_query] 4 | ) 5 | 6 | #' Insert new document and retrieve its `_id` 7 | #' 8 | #' @param conn Mongo db connection from `mongolite::mongo()` 9 | #' @param data Must be a data.frame, named list (for single record). 10 | #' @param ... Named arguments passed to `conn$insert()`. 11 | #' @return The new document, including its `_id` field. 12 | #' @export 13 | insert <- \(conn, data, ...) { 14 | tmp_oid <- UUIDgenerate(use.time = TRUE, n = 1L) 15 | data$`_tmp_oid` <- tmp_oid 16 | conn$insert(data = data, ...) 17 | # get the inserted document: 18 | doc <- conn$find( 19 | query = mongo_query("_tmp_oid" = tmp_oid), 20 | fields = mongo_query("_tmp_oid" = FALSE) 21 | ) 22 | # remove the '_tmp_oid' field: 23 | conn$update( 24 | query = mongo_query("_tmp_oid" = tmp_oid), 25 | update = mongo_query( 26 | "$unset" = list("_tmp_oid" = TRUE) 27 | ), 28 | multiple = TRUE 29 | ) 30 | doc 31 | } 32 | -------------------------------------------------------------------------------- /09_goals/README.md: -------------------------------------------------------------------------------- 1 | ## Goals 2 | 3 | In this example, we will build a CRUD application backend: **Goals**. 4 | 5 | You will be able to **C**reate, **R**ead, **U**pdate & **D**elete Goals. 6 | 7 | Here's what's covered: 8 | 9 | - Ambiorix + MongoDB 10 | - Working with middleware: 11 | - Auth middleware: You will learn how you can use JSON Web Tokens ([JWT](https://jwt.io/)) to protect routes 12 | - Error handling middleware 13 | 14 | ## Prerequisites 15 | 16 | - An installation of the community edition of [MongoDB](https://www.mongodb.com/docs/manual/installation/) 17 | - The [mongolite](https://github.com/jeroen/mongolite) R pkg 18 | 19 | ## Run API 20 | 21 | 1. `cd` into the `09_goals/` dir: 22 | 23 | ```bash 24 | cd 09_goals/ 25 | ``` 26 | 27 | 1. Fire up R and restore package dependencies: 28 | 29 | ```r 30 | renv::restore() 31 | ``` 32 | 33 | 1. `server.R` is the entry point. Run this command in the terminal to start the 34 | API: 35 | 36 | ```bash 37 | Rscript server.R 38 | ``` 39 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/README.md: -------------------------------------------------------------------------------- 1 | # Goals 2 | 3 | This is a frontend for example [`09_goals`](../09_goals). Built using `{shiny}`. 4 | 5 | # Features 6 | 7 | - Auth: 8 | - Custom sign up & login pages 9 | - Cookies: Auto-login next time you visit/reload the app 10 | - APIs: 11 | - Make API requests to the backend using `{httr2}` 12 | - Handle any request errors gracefully via `tryCatch()` 13 | 14 | # Run app 15 | 16 | 1. First make sure that the backend is running. See how [here](../09_goals). 17 | 1. `cd` into the dir `12_shiny_frontend_for_09_goals/`: 18 | 19 | ```bash 20 | cd 12_shiny_frontend_for_09_goals/ 21 | ``` 22 | 23 | 1. Fire up R & restore pkg dependencies: 24 | 25 | ```r 26 | renv::restore() 27 | ``` 28 | 29 | 1. Add these env vars to your `.Renviron`: 30 | 31 | ```r 32 | BASE_URL = http://127.0.0.1:5000 33 | RENV_CONFIG_SANDBOX_ENABLED = FALSE 34 | ``` 35 | 36 | 1. `app.R` is the entry point. Run this on the terminal to start the app: 37 | 38 | ```bash 39 | Rscript app.R 40 | ``` 41 | -------------------------------------------------------------------------------- /14_cors/server.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | ambiorix[Ambiorix], 3 | ) 4 | 5 | #' Handle GET at '/' 6 | #' 7 | #' @export 8 | hello_get <- \(req, res) { 9 | response <- list( 10 | code = 200L, 11 | msg = "hello, world" 12 | ) 13 | res$set_status(200L)$json(response) 14 | } 15 | 16 | #' Allow CORS 17 | #' 18 | #' @details 19 | #' Sets these headers in the response: 20 | #' - `Access-Control-Allow-Methods` 21 | #' - `Access-Control-Allow-Headers` 22 | #' - `Access-Control-Allow-Origin` 23 | #' @export 24 | cors <- \(req, res) { 25 | res$header("Access-Control-Allow-Origin", "http://127.0.0.1:8000") 26 | 27 | if (req$REQUEST_METHOD == "OPTIONS") { 28 | res$header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE") 29 | res$header( 30 | "Access-Control-Allow-Headers", 31 | req$HEADERS$`access-control-request-headers` 32 | ) 33 | 34 | return( 35 | res$set_status(200L)$send("") 36 | ) 37 | } 38 | } 39 | 40 | Ambiorix$ 41 | new(host = "127.0.0.1", port = 5000L)$ 42 | use(cors)$ 43 | get("/", hello_get)$ 44 | start() 45 | -------------------------------------------------------------------------------- /05_router/box/routes/api/members/get_member_by_id.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | ./get_db_path[get_db_path], 3 | ambiorix[parse_multipart], 4 | DBI[dbConnect, dbDisconnect, dbReadTable], 5 | RSQLite[SQLite], 6 | dplyr 7 | ) 8 | 9 | #' Get member by id controller function 10 | #' 11 | #' @inheritParams handler 12 | #' @name views 13 | #' @keywords internal 14 | #' @export 15 | get_member_by_id <- \(req, res) { 16 | # get the supplied id: 17 | member_id <- req$params$id 18 | 19 | conn <- dbConnect(drv = SQLite(), get_db_path()) 20 | on.exit(dbDisconnect(conn)) 21 | members <- dbReadTable(conn = conn, name = "members") 22 | 23 | # filter member with that id: 24 | found <- members |> dplyr$filter(id == member_id) 25 | 26 | # if a member with that id was found, return the member: 27 | if (nrow(found) > 0) { 28 | return(res$json(found)) 29 | } 30 | 31 | # otherwise, change response status to 400 (Bad Request) 32 | # and provide a message: 33 | msg <- list(msg = sprintf("No member with the id of %s", member_id)) 34 | res$set_status(400L)$json(msg) 35 | } 36 | -------------------------------------------------------------------------------- /05_router/r_pkg_structure/R/create_new_member.R: -------------------------------------------------------------------------------- 1 | #' Create new member controller function 2 | #' 3 | #' @inheritParams handler 4 | #' @name views 5 | #' @keywords internal 6 | create_new_member <- \(req, res) { 7 | # parse form-data: 8 | body <- parse_multipart(req) 9 | 10 | name <- body$name 11 | email <- body$email 12 | status <- body$status 13 | 14 | # require all member details: 15 | if (is.null(name) || is.null(email) || is.null(status)) { 16 | msg <- list(msg = "Please include a name, email & status") 17 | return(res$set_status(400L)$json(msg)) 18 | } 19 | 20 | # details of the new member: 21 | new_member <- data.frame( 22 | id = uuid::UUIDgenerate(), 23 | name = name, 24 | email = email, 25 | status = status 26 | ) 27 | 28 | # save new member: 29 | .GlobalEnv$members <- dplyr::bind_rows(members, new_member) 30 | 31 | # respond with a message and details of the newly created member: 32 | response <- list( 33 | msg = "Member created successfully!", 34 | member = new_member 35 | ) 36 | 37 | res$json(response) 38 | } 39 | -------------------------------------------------------------------------------- /13_parse_raw_json/server.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | ambiorix[Ambiorix], 3 | webutils[parse_http], 4 | ) 5 | 6 | #' Handle POST at '/' 7 | #' 8 | #' @param req Request object. 9 | #' @param res Response object. 10 | #' @return `res$json()` 11 | #' @export 12 | home_post <- \(req, res) { 13 | content_type <- req$CONTENT_TYPE 14 | body <- req$rook.input$read() 15 | 16 | if (length(body) == 0L) { 17 | response <- list( 18 | code = 400L, 19 | msg = "Invalid request" 20 | ) 21 | 22 | return( 23 | res$set_status(400L)$json(response) 24 | ) 25 | } 26 | 27 | postdata <- parse_http(body, content_type) 28 | 29 | # filter & select as necessary: 30 | row_inds <- iris$Species %in% postdata$species 31 | col_inds <- colnames(iris) %in% postdata$cols 32 | data <- iris[row_inds, col_inds, drop = FALSE] 33 | 34 | response <- list( 35 | code = 200L, 36 | msg = "success", 37 | data = data 38 | ) 39 | 40 | res$json(response) 41 | } 42 | 43 | app <- Ambiorix$new(port = 3000, host = "127.0.0.1") 44 | 45 | app$ 46 | post("/", home_post)$ 47 | start() 48 | -------------------------------------------------------------------------------- /07_dynamic_rendering/README.md: -------------------------------------------------------------------------------- 1 | ## Dynamic rendering 2 | 3 | When building software, these are the available options: 4 | 5 | 1. Build the backend using ambiorix and the frontend using your favorite framework (React, Angular, Vue, etc.) 6 | 2. Build the backend and frontend using ambiorix 7 | 8 | Let's talk about option 2. 9 | 10 | First things first, you will be rendering html templates/files. 11 | In most cases, you want this to be done dynamically. eg. render a portion of the UI depending on whether a user is an admin or not. 12 | 13 | This is what is referred to as server-side rendering. 14 | 15 | In this example, I use [htmx](https://htmx.org/) to show you how you can build 16 | interactive frontends without touching a single line of JavaScript. 17 | 18 | If you know HTML then you're all set! 19 | 20 | You've already seen how to send HTTP requests to the server & how the server responds (with JSON so far). 21 | 22 | With htmx, your responses from the server will ideally be HTML fragments. 23 | 24 | This works so well with [htmltools](https://rstudio.github.io/htmltools/) you will not believe it! 25 | -------------------------------------------------------------------------------- /documentation/posts/13_parse_raw_json/index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "13: parse raw json" 3 | subtitle: "how to parse raw json in requests" 4 | author: "Kennedy Mwavu" 5 | date: "2024-07-29" 6 | categories: [parse_raw_json] 7 | --- 8 | 9 | # Parse raw JSON 10 | 11 | Sometimes you need to parse requests which have raw JSON in the body. 12 | `ambiorix::parse_multipart()` only works for 'form-data'. That's where `webutils::parse_http()` comes in. 13 | 14 | This is a simple example revolving around how you can select columns and filter 15 | rows in the `iris` dataset when the request body is something like this: 16 | 17 | ```json 18 | { 19 | "cols": ["Sepal.Length", "Petal.Width", "Species"], 20 | "species": ["virginica", "setosa"] 21 | } 22 | ``` 23 | 24 | # Run API 25 | 26 | 1. `cd` into the `13_parse_raw_json/` dir: 27 | 28 | ```bash 29 | cd 13_parse_raw_json/ 30 | ``` 31 | 1. Fire up R and restore package dependencies: 32 | 33 | ```r 34 | renv::restore() 35 | ``` 36 | 1. `server.R` is the entry point. Run this command in the terminal to start the API: 37 | 38 | ```bash 39 | Rscript server.R 40 | ``` 41 | -------------------------------------------------------------------------------- /documentation/posts/01_hello_world/index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "01: hello, world!" 3 | subtitle: "Where else to start? :)" 4 | author: "Kennedy Mwavu" 5 | date: "2024-07-10" 6 | categories: [01_hello_world] 7 | --- 8 | 9 | ## Run app 10 | 11 | 1. `cd` into the `01_hello_world/` dir: 12 | 13 | ```bash 14 | cd 01_hello_world/ 15 | ``` 16 | 17 | 1. Fire up R: 18 | 19 | ```bash 20 | R 21 | ``` 22 | 23 | 1. Restore package dependencies: 24 | 25 | ```r 26 | renv::restore() 27 | ``` 28 | 29 | Once done, exit R. 30 | 1. `index.R` is the entry point. To start the app, run this on the terminal: 31 | 32 | ```bash 33 | Rscript index.R 34 | ``` 35 | 36 | ## Explanation 37 | 38 | This app starts a server and listens on port 3000 for connections. 39 | 40 | It has 2 endpoints: 41 | 42 | - `/`: [localhost:3000/](http://localhost:3000/) 43 | - `/about`: [localhost:3000/about](http://localhost:3000/about) 44 | 45 | For every other path, it will response with a **404 Not Found**. 46 | 47 | ## Static files 48 | 49 | Learn how to serve ✨[Static Files](../02_static_files/index.qmd)✨ using ambiorix. 50 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/store/toast.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | shinytoastr[ 3 | toastr_info, 4 | toastr_error, 5 | toastr_success, 6 | toastr_warning, 7 | ] 8 | ) 9 | 10 | #' Toast notification with custom defaults 11 | #' 12 | #' @param title String. Title to show on top of the toast. 13 | #' @param message String. Message to show. 14 | #' @param time_out Numeric. How long notification should be kept on screen, 15 | #' in milliseconds. See [shinytoastr::toastr_error()]. 16 | #' @param type String. Type of toast. Either "error" (default), "success", 17 | #' "info" or "warning". 18 | #' @export 19 | toast_nofitication <- \( 20 | title = "", 21 | message = "", 22 | time_out = 5000, 23 | type = c("error", "success", "info", "warning") 24 | ) { 25 | type <- match.arg(arg = type) 26 | f <- switch( 27 | EXPR = type, 28 | error = toastr_error, 29 | success = toastr_success, 30 | info = toastr_info, 31 | warning = toastr_warning 32 | ) 33 | 34 | f( 35 | title = title, 36 | message = message, 37 | timeOut = time_out, 38 | position = "bottom-center", 39 | progressBar = TRUE, 40 | closeButton = TRUE 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /05_router/r_pkg_structure/R/update_member_info.R: -------------------------------------------------------------------------------- 1 | #' Update member controller function 2 | #' 3 | #' @inheritParams handler 4 | #' @name views 5 | #' @keywords internal 6 | update_member_info <- \(req, res) { 7 | # get the supplied id: 8 | member_id <- req$params$id 9 | 10 | members <- .GlobalEnv$members 11 | 12 | # filter member with that id: 13 | found <- members |> dplyr::filter(id == member_id) 14 | 15 | # if a member with that id is NOT found, change response status 16 | # and provide a message: 17 | if (nrow(found) == 0) { 18 | msg <- list(msg = sprintf("No member with the id of %s", member_id)) 19 | return(res$set_status(400L)$json(msg)) 20 | } 21 | 22 | # otherwise, proceed to update member: 23 | body <- parse_multipart(req) 24 | 25 | # only update provided fields: 26 | found$name <- body$name %||% found$name 27 | found$email <- body$email %||% found$email 28 | found$status <- body$status %||% found$status 29 | 30 | members[members$id == found$id, ] <- found 31 | .GlobalEnv$members <- members 32 | 33 | response <- list( 34 | msg = "Member updated successfully", 35 | member = found 36 | ) 37 | res$json(response) 38 | } 39 | -------------------------------------------------------------------------------- /08_datatables/store/create_href.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | .. / utils / in_prod[in_prod], 3 | ) 4 | 5 | #' Create an anchor tag's href attribute 6 | #' 7 | #' @description 8 | #' Generates the `href` for an anchor (``) tag. If the application is 9 | #' running in a production environment, the given `path` is prefixed to 10 | #' the `href` to ensure the correct base URL is used. 11 | #' 12 | #' @param href String. `href` attribute of an anchor tag (e.g., "/about"). 13 | #' @param base_path String. Base path on which the app is deployed. eg., 14 | #' if the app is deployed at `https://try.ambiorix.dev/infinite-scroll`, 15 | #' the environment variable `APP_BASE_PATH` should be set to `/infinite-scroll`. 16 | #' The default value is obtained from the `APP_BASE_PATH` environment variable, 17 | #' or it can be passed directly. 18 | #' 19 | #' @return String. The complete `href` for the anchor tag. 20 | #' 21 | #' @examples 22 | #' # In production, this may return "/infinite-scroll/about": 23 | #' create_href("/about") 24 | #' 25 | #' @export 26 | create_href <- \(href, base_path = Sys.getenv("APP_BASE_PATH")) { 27 | if (in_prod()) { 28 | href <- paste0(base_path, href) 29 | } 30 | href 31 | } 32 | -------------------------------------------------------------------------------- /05_router/box/routes/api/members/delete_member.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | ./get_db_path[get_db_path], 3 | DBI[dbConnect, dbDisconnect, dbReadTable, dbWriteTable], 4 | RSQLite[SQLite], 5 | dplyr 6 | ) 7 | 8 | #' Delete member controller function 9 | #' 10 | #' @inheritParams handler 11 | #' @name views 12 | #' @keywords internal 13 | #' @export 14 | delete_member <- \(req, res) { 15 | # get the supplied id: 16 | member_id <- req$params$id 17 | 18 | conn <- dbConnect(drv = SQLite(), get_db_path()) 19 | on.exit(dbDisconnect(conn)) 20 | members <- dbReadTable(conn = conn, name = "members") 21 | 22 | # filter member with that id: 23 | found <- members |> dplyr$filter(id == member_id) 24 | 25 | # if a member with that id is NOT found, change response status 26 | # and provide a message: 27 | if (nrow(found) == 0) { 28 | msg <- list(msg = sprintf("No member with the id of %s", member_id)) 29 | return(res$set_status(400L)$json(msg)) 30 | } 31 | 32 | # otherwise, proceed to delete member: 33 | members <- members |> dplyr$filter(id != member_id) 34 | dbWriteTable(conn = conn, name = "members", value = members, overwrite = TRUE) 35 | 36 | response <- list( 37 | msg = "Member deleted successfully", 38 | members = members 39 | ) 40 | res$json(response) 41 | } 42 | -------------------------------------------------------------------------------- /documentation/posts/12_frontend_for_09_goals/index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "12: frontend for 09_goals" 3 | subtitle: "shiny meets ambiorix" 4 | author: "Kennedy Mwavu" 5 | date: "2024-07-26" 6 | categories: [frontend_for_09_goals] 7 | --- 8 | 9 | # Goals 10 | 11 | This is a frontend for example [`09_goals`](../09_goals/index.qmd). Built using `{shiny}`. 12 | 13 | # Features 14 | 15 | - Auth: 16 | - Custom sign up & login pages 17 | - Cookies: Auto-login next time you visit/reload the app 18 | - APIs: 19 | - Make API requests to the backend using `{httr2}` 20 | - Handle any request errors gracefully via `tryCatch()` 21 | 22 | # Run app 23 | 24 | 1. First make sure that the backend is running. See how [here](../09_goals/index.qmd). 25 | 1. `cd` into the dir `12_shiny_frontend_for_09_goals/`: 26 | 27 | ```bash 28 | cd 12_shiny_frontend_for_09_goals/ 29 | ``` 30 | 31 | 1. Fire up R & restore pkg dependencies: 32 | 33 | ```r 34 | renv::restore() 35 | ``` 36 | 37 | 1. Add these env vars to your `.Renviron`: 38 | 39 | ```r 40 | BASE_URL = http://127.0.0.1:5000 41 | RENV_CONFIG_SANDBOX_ENABLED = FALSE 42 | ``` 43 | 44 | 1. `app.R` is the entry point. Run this on the terminal to start the app: 45 | 46 | ```bash 47 | Rscript app.R 48 | ``` 49 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/modules/auth/server.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | shiny[ 3 | reactiveVal, 4 | moduleServer, 5 | observeEvent, 6 | updateTabsetPanel, 7 | freezeReactiveValue, 8 | ], 9 | . / login_server[login_server = server], 10 | . / signup_server[signup_server = server], 11 | .. / .. / helpers / mod[`%||%`], 12 | ) 13 | 14 | #' Auth module server 15 | #' 16 | #' @param id String. Module id. 17 | #' @export 18 | server <- \(id) { 19 | moduleServer( 20 | id = id, 21 | module = \(input, output, session) { 22 | switch_to_tab <- \(tab) { 23 | freezeReactiveValue(x = input, name = "tabs") 24 | updateTabsetPanel( 25 | session = session, 26 | inputId = "tabs", 27 | selected = tab 28 | ) 29 | } 30 | 31 | rv_user <- reactiveVal() 32 | r_signup <- signup_server(id = "signup") 33 | r_login <- login_server(id = "login") 34 | 35 | observeEvent(r_signup()$go_to_login, switch_to_tab("login")) 36 | observeEvent(r_login()$go_to_signup, switch_to_tab("signup")) 37 | 38 | observeEvent( 39 | eventExpr = c(r_signup()$user, r_login()$user), 40 | handlerExpr = { 41 | user <- r_signup()$user %||% r_login()$user 42 | rv_user(user) 43 | } 44 | ) 45 | 46 | return(rv_user) 47 | } 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /05_router/box/routes/api/members/create_new_member.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | ./get_db_path[get_db_path], 3 | ambiorix[parse_multipart], 4 | RSQLite[SQLite], 5 | DBI[dbConnect, dbDisconnect, dbAppendTable], 6 | uuid 7 | ) 8 | 9 | #' Create new member controller function 10 | #' 11 | #' @inheritParams handler 12 | #' @name views 13 | #' @keywords internal 14 | #' @export 15 | create_new_member <- \(req, res) { 16 | # parse form-data: 17 | body <- parse_multipart(req) 18 | 19 | name <- body$name 20 | email <- body$email 21 | status <- body$status 22 | 23 | # require all member details: 24 | if (is.null(name) || is.null(email) || is.null(status)) { 25 | msg <- list(msg = "Please include a name, email & status") 26 | return(res$set_status(400L)$json(msg)) 27 | } 28 | 29 | # details of the new member: 30 | new_member <- data.frame( 31 | id = uuid$UUIDgenerate(), 32 | name = name, 33 | email = email, 34 | status = status 35 | ) 36 | 37 | # save new member: 38 | conn <- dbConnect(drv = SQLite(), get_db_path()) 39 | on.exit(dbDisconnect(conn)) 40 | dbAppendTable(conn = conn, name = "members", value = new_member) 41 | 42 | # respond with a message and details of the newly created member: 43 | response <- list( 44 | msg = "Member created successfully!", 45 | member = new_member 46 | ) 47 | 48 | res$json(response) 49 | } 50 | -------------------------------------------------------------------------------- /docs/site_libs/quarto-html/tippy.css: -------------------------------------------------------------------------------- 1 | .tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1} -------------------------------------------------------------------------------- /08_datatables/controllers/flights_get.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | dplyr, 3 | nycflights13[flights], 4 | .. / store / datatable[make_user_friendly_names] 5 | ) 6 | 7 | #' Get flights data 8 | #' 9 | #' Handler for GET requests at "/data/flights". 10 | #' @export 11 | flights_get <- \(req, res) { 12 | draw <- req$query$draw 13 | start_row <- as.integer(req$query$start) + 1L # datatables use 0-based indexing (JS) 14 | num_of_rows <- as.integer(req$query$length) - 1L 15 | end_row <- start_row + num_of_rows 16 | 17 | search_res <- flights 18 | 19 | # perform search: 20 | search_value <- req$query$`search[value]` 21 | if (!is.na(search_value)) { 22 | found <- lapply(flights, \(cl) { 23 | grepl(pattern = search_value, x = cl, ignore.case = TRUE) 24 | }) |> 25 | as.data.frame() |> 26 | rowSums(na.rm = TRUE) 27 | found <- which(found > 0) 28 | search_res <- flights |> dplyr$slice(found) 29 | } 30 | 31 | records_filtered <- nrow(search_res) 32 | 33 | # filter out the requested rows: 34 | row_inds <- seq(from = start_row, to = end_row, by = 1L) 35 | filtered <- search_res |> dplyr$slice(row_inds) 36 | 37 | names(filtered) <- names(filtered) |> make_user_friendly_names() 38 | 39 | # datatable expects json: 40 | response <- list( 41 | draw = draw, 42 | recordsTotal = nrow(flights), 43 | recordsFiltered = records_filtered, 44 | data = filtered 45 | ) 46 | res$json(response) 47 | } 48 | -------------------------------------------------------------------------------- /07_dynamic_rendering/controllers/validate_email.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | ambiorix[parse_multipart], 3 | htmltools[tags], 4 | .. / store / contact[email_input] 5 | ) 6 | 7 | #' Validate contact email 8 | #' 9 | #' Handler for POST requests at "/contact/email". 10 | #' 11 | #' @return An object of class `shiny.tag` 12 | #' @export 13 | validate_email <- \(req, res) { 14 | body <- parse_multipart(req) 15 | email <- body$email 16 | html <- check_valid_email(email) 17 | res$send(html) 18 | } 19 | 20 | #' Check valid email 21 | #' 22 | #' @return An object of class `shiny.tag` 23 | #' @export 24 | check_valid_email <- \(email) { 25 | is_valid <- is_valid_email(email) 26 | 27 | email_input( 28 | value = email, 29 | input_class = if (is_valid) "is-valid" else "is-invalid", 30 | if (!is_valid) { 31 | tags$div( 32 | class = "invalid-feedback", 33 | "Please enter a valid email address" 34 | ) 35 | } 36 | ) 37 | } 38 | 39 | #' Check if email is valid 40 | #' 41 | #' @param email Character vector. Emails to check. 42 | #' @return Logical vector of same length as `email`. 43 | #' `TRUE` if email is valid, `FALSE` otherwise. 44 | #' @examples 45 | #' mails <- c("example@gmail.com", "this", "S.N@org-nz.com") 46 | #' is_valid_email(mails) 47 | #' @export 48 | is_valid_email <- \(email) { 49 | grepl( 50 | pattern = "\\<[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}\\>", 51 | x = email, 52 | ignore.case = TRUE 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /07_dynamic_rendering/controllers/contact_post.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | ambiorix[parse_multipart], 3 | htmltools[tags], 4 | shiny[isTruthy], 5 | . / validate_email[is_valid_email, check_valid_email], 6 | . / validate_message[check_valid_message], 7 | .. / store / contact 8 | ) 9 | 10 | #' Receive message sent by user 11 | #' 12 | #' Handler for POST requests at "/contact" 13 | #' 14 | #' @return An object of class `shiny.tag` 15 | #' @export 16 | contact_post <- \(req, res) { 17 | body <- parse_multipart(req) 18 | email <- body$email 19 | firstname <- body$Firstname 20 | lastname <- body$Lastname 21 | message <- body$msg 22 | 23 | all_valid <- is_valid_email(email) && 24 | isTruthy(message) 25 | 26 | if (all_valid) { 27 | html <- tags$p( 28 | class = "text-success", 29 | "Thank you! We shall happily review your feedback." 30 | ) 31 | return(res$send(html)) 32 | } 33 | 34 | email_input <- check_valid_email(email) 35 | message_input <- check_valid_message(message) 36 | 37 | html <- tags$form( 38 | `hx-target` = "this", 39 | `hx-post` = "/contact", 40 | `hx-swap` = "outerHTML", 41 | email_input, 42 | tags$div( 43 | class = "row", 44 | tags$div( 45 | class = "col-12 col-md-6", 46 | contact$name_input(kind = "First", value = firstname) 47 | ), 48 | tags$div( 49 | class = "col-12 col-md-6", 50 | contact$name_input(kind = "Last", value = lastname) 51 | ) 52 | ), 53 | message_input, 54 | contact$submit_btn() 55 | ) 56 | res$send(html) 57 | } 58 | -------------------------------------------------------------------------------- /05_router/box/routes/api/members/update_member_info.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | ./get_db_path[get_db_path], 3 | .. / .. / .. / helpers / operators[`%||%`], 4 | ambiorix[parse_multipart], 5 | DBI[dbConnect, dbDisconnect, dbReadTable, dbWriteTable], 6 | RSQLite[SQLite], 7 | dplyr 8 | ) 9 | 10 | #' Update member controller function 11 | #' 12 | #' @inheritParams handler 13 | #' @name views 14 | #' @keywords internal 15 | #' @export 16 | update_member_info <- \(req, res) { 17 | # get the supplied id: 18 | member_id <- req$params$id 19 | 20 | conn <- dbConnect(drv = SQLite(), get_db_path()) 21 | on.exit(dbDisconnect(conn)) 22 | members <- dbReadTable(conn = conn, name = "members") 23 | 24 | # filter member with that id: 25 | found <- members |> dplyr$filter(id == member_id) 26 | 27 | # if a member with that id is NOT found, change response status 28 | # and provide a message: 29 | if (nrow(found) == 0) { 30 | msg <- list(msg = sprintf("No member with the id of %s", member_id)) 31 | return(res$set_status(400L)$json(msg)) 32 | } 33 | 34 | # otherwise, proceed to update member: 35 | body <- parse_multipart(req) 36 | 37 | # only update provided fields: 38 | found$name <- body$name %||% found$name 39 | found$email <- body$email %||% found$email 40 | found$status <- body$status %||% found$status 41 | 42 | members[members$id == found$id, ] <- found 43 | dbWriteTable(conn = conn, name = "members", value = members, overwrite = TRUE) 44 | 45 | response <- list( 46 | msg = "Member updated successfully", 47 | member = found 48 | ) 49 | res$json(response) 50 | } 51 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/modules/auth/login_server.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | . / proxy[ 3 | login, 4 | req_error_handler, 5 | ], 6 | shiny[ 7 | req, 8 | reactive, 9 | reactiveVal, 10 | moduleServer, 11 | observeEvent, 12 | ], 13 | cookies[set_cookie], 14 | .. / .. / store / mod[toast_nofitication], 15 | ) 16 | 17 | #' Login server module 18 | #' 19 | #' @param id String. Module id. 20 | #' @export 21 | server <- \(id) { 22 | moduleServer( 23 | id = id, 24 | module = \(input, output, session) { 25 | rv_user <- reactiveVal() 26 | 27 | observeEvent(input$login, { 28 | email <- input$email 29 | password <- input$password 30 | req(email, password) 31 | 32 | tryCatch( 33 | expr = { 34 | details <- login( 35 | email = email, 36 | password = password 37 | ) 38 | rv_user(details$user) 39 | 40 | toast_nofitication( 41 | message = "Logged in!", 42 | type = "success" 43 | ) 44 | }, 45 | error = req_error_handler 46 | ) 47 | }) 48 | 49 | # on login, set auth cookie: 50 | observeEvent(rv_user(), { 51 | set_cookie( 52 | cookie_name = "auth", 53 | cookie_value = rv_user()$token, 54 | secure_only = TRUE, 55 | expiration = 30L 56 | ) 57 | }) 58 | 59 | r_res <- reactive({ 60 | list( 61 | go_to_signup = input$signup, 62 | user = rv_user() 63 | ) 64 | }) 65 | return(r_res) 66 | } 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /15_chat/ui/page.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | htmltools[...], 3 | ) 4 | 5 | #' @export 6 | page <- function(...) { 7 | tags$html( 8 | tags$head( 9 | tags$title("Ambiorix chat"), 10 | tags$link( 11 | rel = "stylesheet", 12 | href = "https://unpkg.com/nes.css@2.3.0/css/nes.min.css" 13 | ), 14 | tags$link( 15 | rel = "stylesheet", 16 | href = "https://fonts.googleapis.com/css?family=Press+Start+2P" 17 | ), 18 | tags$link( 19 | rel = "stylesheet", 20 | href = "static/style.css" 21 | ), 22 | tags$script( 23 | src = "static/ambiorix.js" 24 | ), 25 | tags$script( 26 | src = "static/chat.js", 27 | defer = NA 28 | ) 29 | ), 30 | tags$body( 31 | class = "container", 32 | h1("R chat app"), 33 | tags$section( 34 | class = "nes-container mb-1", 35 | id = "chat", 36 | tags$section( 37 | class = "message-list", 38 | id = "chat-list" 39 | ) 40 | ), 41 | div( 42 | class = "d-flex", 43 | div( 44 | class = "d-grow", 45 | div( 46 | class = "nes-field mr-1", 47 | tags$input( 48 | id = "message", 49 | class = "nes-input", 50 | placeholder = "Your message" 51 | ) 52 | ) 53 | ), 54 | div( 55 | class = "d-shrink", 56 | tags$button( 57 | id = "send", 58 | type = "button", 59 | class = "nes-btn is-primary", 60 | "SEND" 61 | ) 62 | ) 63 | ), 64 | ... 65 | ) 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /08_datatables/store/page.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | htmltools[ 3 | tags, 4 | tagList, 5 | HTML, 6 | ], 7 | . / create_href[create_href], 8 | ) 9 | 10 | #' Generic HTML page 11 | #' 12 | #' @param ... Passed to the body tag of the html document. 13 | #' @return [htmltools::tags] 14 | #' @export 15 | page <- \(...) { 16 | tagList( 17 | HTML(""), 18 | tags$html( 19 | tags$head( 20 | tags$meta(charset = "utf-8"), 21 | tags$meta( 22 | name = "viewport", 23 | content = "width=device-width, initial-scale=1" 24 | ), 25 | tags$link( 26 | rel = "stylesheet", 27 | href = create_href("/static/styles.css") 28 | ), 29 | tags$link( 30 | rel = "stylesheet", 31 | href = create_href("/static/bootstrap-5.3.2-dist/bootstrap.min.css") 32 | ), 33 | tags$link( 34 | rel = "stylesheet", 35 | href = "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" 36 | ), 37 | tags$link( 38 | rel = "stylesheet", 39 | href = "https://cdn.datatables.net/v/bs5/jq-3.7.0/dt-1.13.8/datatables.min.css" 40 | ), 41 | tags$script( 42 | src = "https://cdn.datatables.net/v/bs5/jq-3.7.0/dt-1.13.8/datatables.min.js" 43 | ), 44 | tags$title("Home | Datatables") 45 | ), 46 | tags$body( 47 | class = "bg-light", 48 | ..., 49 | tags$script( 50 | type = "text/javascript", 51 | src = create_href( 52 | "/static/bootstrap-5.3.2-dist/bootstrap.bundle.min.js" 53 | ) 54 | ) 55 | ) 56 | ) 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/modules/auth/signup_server.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | shiny[ 3 | req, 4 | reactive, 5 | isTruthy, 6 | reactiveVal, 7 | moduleServer, 8 | observeEvent, 9 | ], 10 | cookies[set_cookie], 11 | . / proxy[ 12 | create_account, 13 | req_error_handler, 14 | ], 15 | .. / .. / store / mod[toast_nofitication], 16 | ) 17 | 18 | #' Signup server module 19 | #' 20 | #' @param id String. Module id. 21 | #' @export 22 | server <- \(id) { 23 | moduleServer( 24 | id = id, 25 | module = \(input, output, session) { 26 | rv_user <- reactiveVal() 27 | 28 | observeEvent(input$signup, { 29 | name <- input$name 30 | email <- input$email 31 | password <- input$password 32 | req(name, email, password) 33 | 34 | tryCatch( 35 | expr = { 36 | details <- create_account( 37 | name = name, 38 | email = email, 39 | password = password 40 | ) 41 | rv_user(details$user) 42 | 43 | toast_nofitication( 44 | title = "Success!", 45 | message = "Account created.", 46 | type = "success" 47 | ) 48 | }, 49 | error = req_error_handler 50 | ) 51 | }) 52 | 53 | # on registration, set auth cookie: 54 | observeEvent(rv_user(), { 55 | set_cookie( 56 | cookie_name = "auth", 57 | cookie_value = rv_user()$token, 58 | secure_only = TRUE, 59 | expiration = 30L 60 | ) 61 | }) 62 | 63 | r_res <- reactive({ 64 | list( 65 | go_to_login = input$login, 66 | user = rv_user() 67 | ) 68 | }) 69 | return(r_res) 70 | } 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /05_router/box/README.md: -------------------------------------------------------------------------------- 1 | ## Using [{box}](https://klmr.me/box/index.html) with [{ambiorix}](https://ambiorix.dev/) 2 | 3 | I highly recommend this way of building apps. 4 | 5 | As you go through the code, you will easily notice how modularized it is. 6 | 7 | {box} enables you to split your app into small, manageable & interconnected modules: 8 | 9 | ```bash 10 | . 11 | ├── data 12 | │   ├── get_db_path.R 13 | │   └── members.sqlite 14 | ├── helpers 15 | │   ├── check_port.R 16 | │   ├── get_port.R 17 | │   └── operators.R 18 | ├── index.R 19 | ├── middleware 20 | │   └── logger.R 21 | ├── models 22 | │   └── members.R 23 | ├── README.md 24 | └── routes 25 | └── api 26 | ├── members 27 | │   ├── controllers.R 28 | │   ├── create_new_member.R 29 | │   ├── delete_member.R 30 | │   ├── get_all_members.R 31 | │   ├── get_db_path.R 32 | │   ├── get_member_by_id.R 33 | │   └── update_member_info.R 34 | └── members.R 35 | ``` 36 | 37 | > [!CAUTION] 38 | > You'll notice that I've used an sqlite file. The table "members" is very small, 39 | > that's why I read the whole table and write it again as much as I want. 40 | > Again, this is just for purposes of a reprex. 41 | > In the real world, you should write sql-interpolated queries to the database. 42 | > 43 | > **NEVER** commit a database file or a `.Renviron` file! 44 | 45 | Once you run the app, you'll be able to send requests via these endpoints: 46 | - `GET`: `http://localhost:3000/api/members` : Gets all members 47 | - `GET`: `http://localhost:3000/api/members/:id` : Get a single member by id 48 | - `POST`: `http://localhost:3000/api/members` : Create a new member 49 | - `PUT`: `http://localhost:3000/api/members/:id` : Update member info 50 | - `DELETE`: `http://localhost:3000/api/members/:id` : Delete a member 51 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/modules/server.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | shiny[ 3 | req, 4 | isTruthy, 5 | reactiveVal, 6 | observeEvent, 7 | updateTabsetPanel, 8 | freezeReactiveValue, 9 | ], 10 | cookies[get_cookie], 11 | . / auth / mod[ 12 | auth_server, 13 | get_account_details, 14 | ], 15 | . / goals / mod[goals_server], 16 | .. / store / mod[toast_nofitication], 17 | ) 18 | 19 | #' App server 20 | #' 21 | #' @export 22 | server <- \(input, output, session) { 23 | switch_to_tab <- \(tab) { 24 | freezeReactiveValue(x = input, name = "pages") 25 | updateTabsetPanel( 26 | session = session, 27 | inputId = "pages", 28 | selected = tab 29 | ) 30 | } 31 | 32 | rv_user <- reactiveVal() 33 | r_user <- auth_server(id = "auth") 34 | r_goals <- goals_server(id = "goals", rv_user = rv_user) 35 | 36 | observeEvent(r_user(), { 37 | rv_user(r_user()) 38 | switch_to_tab("goals") 39 | }) 40 | 41 | observeEvent( 42 | eventExpr = get_cookie(cookie_name = "auth"), 43 | handlerExpr = { 44 | token <- get_cookie(cookie_name = "auth") 45 | req(token) 46 | 47 | # if it's signup or login, no need to get user details: 48 | is_signup_or_login <- isTruthy(rv_user()$token) 49 | if (is_signup_or_login) { 50 | return() 51 | } 52 | 53 | tryCatch( 54 | expr = { 55 | details <- get_account_details(token = token) 56 | details$user$token <- token 57 | 58 | rv_user(details$user) 59 | switch_to_tab("goals") 60 | toast_nofitication( 61 | message = "Logged in!", 62 | type = "success" 63 | ) 64 | }, 65 | error = \(e) { 66 | "Invalid token, not signing in" 67 | } 68 | ) 69 | } 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /06_multi_router/README.md: -------------------------------------------------------------------------------- 1 | ## Multiple routers 2 | 3 | You can have multiple routers mounted onto `ambiorix::Ambiorix` via the `use()` method. 4 | 5 | In this example, I show how you can version your api. 6 | 7 | Remember, ambiorix is unopinionated. This is just my way of doing things. 8 | 9 | You'll see that using [{box}](https://github.com/klmr/box) is the easiest way to split up & 10 | organize your files & folders. 11 | 12 | This is the directory structure that I've used: 13 | 14 | ``` 15 | . 16 | ├── api 17 | │   ├── members.R 18 | │   ├── v1 19 | │   │   ├── members 20 | │   │   │   ├── controllers.R 21 | │   │   │   ├── create_new_member.R 22 | │   │   │   ├── delete_member.R 23 | │   │   │   ├── get_all_members.R 24 | │   │   │   ├── get_member_by_id.R 25 | │   │   │   └── update_member_info.R 26 | │   │   └── members.R 27 | │   └── v2 28 | │   ├── members 29 | │   │   ├── controllers.R 30 | │   │   ├── create_new_member.R 31 | │   │   ├── delete_member.R 32 | │   │   ├── get_all_members.R 33 | │   │   ├── get_member_by_id.R 34 | │   │   └── update_member_info.R 35 | │   └── members.R 36 | ├── index.R 37 | └── README.md 38 | ``` 39 | 40 | This is how `index.R` looks like: 41 | 42 | ```r 43 | box::use( 44 | ambiorix[Ambiorix], 45 | . / api / members 46 | ) 47 | 48 | Ambiorix$ 49 | new()$ 50 | listen(port = 3000L)$ 51 | use(members$v1)$ # mount API v1 members' router 52 | use(members$v2)$ # mount API v2 members' router 53 | start(open = FALSE) 54 | ``` 55 | 56 | Once you run the app, you should be able to perform requests on 57 | http://localhost:3000/api/v*/members, eg. 58 | 59 | - `GET` request on `http://localhost:3000/api/v1/members` 60 | - `PUT` request on `http://localhost:3000/api/v2/members/:3` 61 | 62 | ... and so on. 63 | 64 | Checkout the routers at: 65 | - [v1 members router](./api/v1/members.R) 66 | - [v2 members router](./api/v2/members.R) 67 | -------------------------------------------------------------------------------- /15_chat/public/ambiorix.js: -------------------------------------------------------------------------------- 1 | // default insecure 2 | let protocol = 'ws://'; 3 | 4 | // upgrade if secure 5 | if (window.location.protocol == "https:") 6 | protocol = 'wss://'; 7 | 8 | // get websocket 9 | let ambiorixSocket = new WebSocket(protocol + window.location.host); 10 | 11 | // ambiorix Websocket 12 | class Ambiorix { 13 | constructor() { 14 | this.onOpen = () => { }; 15 | this.onClose = () => { }; 16 | this.onError = (error) => { console.error(error) }; 17 | this._handlers = new Map(); 18 | } 19 | // send 20 | static send(name, message) { 21 | 22 | // build message 23 | let msg = { name: name, message: message, isAmbiorix: true }; 24 | 25 | ambiorixSocket.send(JSON.stringify(msg)); 26 | 27 | } 28 | 29 | onopen(fn) { 30 | this.onOpen = fn; 31 | } 32 | 33 | onclose(fn) { 34 | this.onClose = fn; 35 | } 36 | 37 | onerror(fn) { 38 | this.onError = fn; 39 | } 40 | 41 | start() { 42 | var that = this; 43 | ambiorixSocket.onmessage = (msg) => { 44 | let msgParsed = JSON.parse(msg.data); 45 | 46 | if (!msgParsed.isAmbiorix) 47 | return; 48 | 49 | if (that._handlers.has(msgParsed.name)) { 50 | that._handlers.get(msgParsed.name)(msgParsed.message); 51 | } 52 | } 53 | 54 | ambiorixSocket.onopen = this.onOpen; 55 | ambiorixSocket.onclose = this.onClose; 56 | ambiorixSocket.onerror = this.onError; 57 | } 58 | // receiver 59 | receive(name, fun) { 60 | this._handlers.set(name, fun) 61 | } 62 | } 63 | 64 | // helper function to parseCookies 65 | // parseCookie(document.cookie); 66 | const parseCookie = str => { 67 | if (str == "") 68 | return {}; 69 | 70 | return str 71 | .split(';') 72 | .map(v => v.split('=')) 73 | .reduce( 74 | (acc, v) => { 75 | acc[decodeURIComponent(v[0].trim())] = decodeURIComponent(v[1].trim()); 76 | return acc; 77 | }, 78 | {}); 79 | } 80 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/modules/goals/server.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | cli[cli_abort], 3 | cookies[remove_cookie], 4 | shiny[ 5 | renderUI, 6 | is.reactive, 7 | moduleServer, 8 | observeEvent, 9 | updateTabsetPanel, 10 | freezeReactiveValue, 11 | ], 12 | . / ui[user_profile_btn], 13 | . / account_server[account_server = server], 14 | . / dashboard_server[dashboard_server = server], 15 | ) 16 | 17 | #' Goals server module 18 | #' 19 | #' @param id String. Module id. 20 | #' @param rv_user [shiny::reactiveVal()]. User details after auth. 21 | #' @export 22 | server <- \(id, rv_user) { 23 | if (!is.reactive(rv_user)) { 24 | cli_abort( 25 | message = c( 26 | "x" = "{.var rv_user} must be a {.fn reactiveVal}", 27 | "i" = "Did you pass in a bare value instead of a reactive?" 28 | ) 29 | ) 30 | } 31 | 32 | moduleServer( 33 | id = id, 34 | module = \(input, output, session) { 35 | ns <- session$ns 36 | 37 | switch_to_tab <- \(tab) { 38 | freezeReactiveValue(x = input, name = "tabs") 39 | updateTabsetPanel( 40 | session = session, 41 | inputId = "tabs", 42 | selected = tab 43 | ) 44 | } 45 | 46 | output$user_profile_btn <- renderUI({ 47 | user_profile_btn( 48 | ns = ns, 49 | user_name = rv_user()$name 50 | ) 51 | }) 52 | 53 | observeEvent(input$logout, { 54 | # remove auth cookie, reload page: 55 | remove_cookie(cookie_name = "auth") 56 | session$reload() 57 | }) 58 | 59 | observeEvent(input$go_to_account_settings, switch_to_tab("account")) 60 | 61 | dashboard_server(id = "dashboard", rv_user = rv_user) 62 | 63 | r_account <- account_server(id = "account", rv_user = rv_user) 64 | observeEvent(r_account()$go_back_to_dashboard, { 65 | switch_to_tab("dashboard") 66 | }) 67 | } 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /documentation/posts/07_dynamic_rendering/index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "07: dynamic rendering" 3 | subtitle: "Dynamically render templates" 4 | author: "Kennedy Mwavu" 5 | date: "2024-07-10" 6 | categories: [07_dynamic_rendering] 7 | --- 8 | 9 | ## Run app 10 | 11 | 1. `cd` into the `07_dynamic_rendering/` dir: 12 | 13 | ```bash 14 | cd 07_dynamic_rendering/ 15 | ``` 16 | 17 | 1. Fire up R: 18 | 19 | ```bash 20 | R 21 | ``` 22 | 23 | 1. Restore package dependencies: 24 | 25 | ```r 26 | renv::restore() 27 | ``` 28 | 29 | Once done, exit R. 30 | 1. `index.R` is the entry point. To start the app, run this on the terminal: 31 | 32 | ```bash 33 | Rscript index.R 34 | ``` 35 | 36 | ## Explanation 37 | 38 | This app starts a server and listens on port 3000 for connections. 39 | 40 | When building software, these are the available options: 41 | 42 | 1. Build the backend using ambiorix and the frontend using your favorite frontend framework (React, Angular, Vue, etc.) 43 | 2. Build both the back and frontend using ambiorix 44 | 45 | Let's talk about option 2. 46 | 47 | First things first, you will be rendering html templates/files. 48 | In most cases, you want this to be done dynamically. eg. render a portion of the UI depending on whether a user is an admin or not. 49 | 50 | This is what is referred to as server-side rendering (SSR). 51 | 52 | In this example, I use [htmx](https://htmx.org/) to show you how you can build 53 | interactive frontends without touching a single line of JavaScript. 54 | 55 | If you know HTML then you're all set! 56 | 57 | You've already seen how to send HTTP requests to the server & how the server responds (with JSON so far). 58 | 59 | With htmx, your responses from the server will ideally be HTML fragments. 60 | 61 | This works so well with [htmltools](https://rstudio.github.io/htmltools/) you will not believe it! 62 | 63 | ## Datatables 64 | 65 | Well, R people love tables. Time for you to look at ✨[datatables](../08_datatables/index.qmd)✨. 66 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/modules/goals/proxy.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | httr2[ 3 | request, 4 | req_method, 5 | req_perform, 6 | req_url_path, 7 | last_response, 8 | resp_body_json, 9 | req_body_multipart, 10 | req_auth_bearer_token, 11 | ], 12 | .. / auth / mod[req_error_handler], 13 | .. / .. / helpers / mod[get_base_url], 14 | .. / .. / store / mod[toast_nofitication], 15 | ) 16 | 17 | #' Create a new goal 18 | #' 19 | #' @param text String. The goal. 20 | #' @param token String. JWT token. 21 | #' @return Named list. 22 | #' @export 23 | create_goal <- \(text, token) { 24 | request(base_url = get_base_url()) |> 25 | req_url_path("/api/goals") |> 26 | req_auth_bearer_token(token = token) |> 27 | req_body_multipart(text = text) |> 28 | req_perform() |> 29 | resp_body_json() 30 | } 31 | 32 | #' Read all goals 33 | #' 34 | #' @param token String. JWT token. 35 | #' @return Named list. 36 | #' @export 37 | read_goals <- \(token) { 38 | request(base_url = get_base_url()) |> 39 | req_url_path("/api/goals") |> 40 | req_auth_bearer_token(token = token) |> 41 | req_perform() |> 42 | resp_body_json() 43 | } 44 | 45 | #' Update a goal 46 | #' 47 | #' @param id String. Goal id. 48 | #' @param text String. Updated goal. 49 | #' @param token String. JWT token. 50 | #' @return Named list. 51 | #' @export 52 | update_goal <- \( 53 | id, 54 | text, 55 | token 56 | ) { 57 | path <- paste0("/api/goals/", id) 58 | request(base_url = get_base_url()) |> 59 | req_url_path(path) |> 60 | req_auth_bearer_token(token = token) |> 61 | req_method(method = "PUT") |> 62 | req_body_multipart(text = text) |> 63 | req_perform() |> 64 | resp_body_json() 65 | } 66 | 67 | #' Delete a goal 68 | #' 69 | #' @param id String. Goal id. 70 | #' @param token String. JWT token. 71 | #' @return Named list. 72 | #' @export 73 | delete_goal <- \(id, token) { 74 | path <- paste0("/api/goals/", id) 75 | request(base_url = get_base_url()) |> 76 | req_url_path(path) |> 77 | req_auth_bearer_token(token = token) |> 78 | req_method(method = "DELETE") |> 79 | req_perform() |> 80 | resp_body_json() 81 | } 82 | -------------------------------------------------------------------------------- /09_goals/middleware/auth_middleware.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | jose[jwt_decode_hmac], 3 | .. / config / db[users_conn], 4 | .. / helpers / mongo_query[mongo_query], 5 | .. / helpers / get_jwt_secret[get_jwt_secret] 6 | ) 7 | 8 | #' Get protected routes 9 | #' 10 | #' Returns the start string of protected routes 11 | #' @return Character vector. 12 | protected_paths <- \() { 13 | c( 14 | "/api/users/me", 15 | "/api/goals" 16 | ) 17 | } 18 | 19 | #' Check if path is protected 20 | #' 21 | #' @return Logical. 22 | is_protected_path <- \(req_path_info) { 23 | startsWith(x = req_path_info, prefix = protected_paths()) |> any() 24 | } 25 | 26 | #' Protect routes 27 | #' 28 | #' @export 29 | protect <- \(req, res) { 30 | # check if route is private: 31 | path_info <- req$PATH_INFO 32 | if (!is_protected_path(path_info)) { 33 | return(res) 34 | } 35 | 36 | auth_headers <- req$HEADERS$authorization 37 | is_valid <- !is.null(auth_headers) && 38 | startsWith(x = auth_headers, prefix = "Bearer") 39 | 40 | response_401 <- list( 41 | code = 401L, 42 | msg = "Not authorized" 43 | ) 44 | 45 | if (!is_valid) { 46 | return( 47 | res$set_status(401L)$json(response_401) 48 | ) 49 | } 50 | 51 | tryCatch( 52 | expr = { 53 | # get token from header: 54 | token <- strsplit(x = auth_headers, split = " ")[[1]][[2]] 55 | # verify the token: 56 | decoded <- jwt_decode_hmac(jwt = token, secret = get_jwt_secret()) 57 | # get user id from the token: 58 | user_id <- decoded$user_id 59 | # find the user: 60 | found <- users_conn$find( 61 | query = mongo_query( 62 | "_id" = list("$oid" = user_id) 63 | ), 64 | fields = mongo_query(`_id` = TRUE, name = TRUE, email = TRUE) 65 | ) 66 | # in case the account was deleted and user tries to access a protected 67 | # resource: 68 | if (nrow(found) != 1L) { 69 | stop("Unauthorized", call. = FALSE) 70 | } 71 | # set the user in the request object: 72 | req$user <- found 73 | }, 74 | error = \(e) { 75 | print(e) 76 | res$set_status(401L)$json(response_401) 77 | } 78 | ) 79 | } 80 | -------------------------------------------------------------------------------- /08_datatables/store/home.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | htmltools[tags, tagList], 3 | nycflights13[flights], 4 | . / nav[nav], 5 | . / page[page], 6 | . / create_card[create_card], 7 | . / create_href[create_href], 8 | . / 9 | datatable[ 10 | datatable, 11 | make_user_friendly_names, 12 | ], 13 | ) 14 | 15 | #' The "Home" page 16 | #' 17 | #' @return An object of class `shiny.tag` 18 | #' @export 19 | home <- \() { 20 | intro_card <- create_card( 21 | title = "Data Tables", 22 | title_icon = tags$i(class = "bi bi-grid"), 23 | title_class = "text-primary", 24 | class = "shadow-sm", 25 | tags$p("R users love tables, and so do we!"), 26 | tags$p( 27 | "Enter", 28 | tags$a( 29 | href = "https://datatables.net/", 30 | "DataTables." 31 | ) 32 | ), 33 | tags$p( 34 | "In the example below, we show how you can use serverside processing 35 | if your data is huge." 36 | ) 37 | ) 38 | 39 | table_card <- create_card( 40 | title = NULL, 41 | title_icon = NULL, 42 | title_class = "text-primary", 43 | class = "shadow-sm mt-3 mb-5", 44 | # you will mostly get the column names from the database 45 | datatable( 46 | col_names = names(flights) |> make_user_friendly_names(), 47 | table_id = "flights", 48 | table_class = "table-hover table-bordered table-sm", 49 | header_row_class = "table-active", 50 | processing = TRUE, 51 | serverSide = TRUE, 52 | searchDelay = 1500, 53 | # disable sorting of cols: 54 | ordering = FALSE, 55 | # send a GET request (the default for ajax) to "/data/flights" 56 | ajax = create_href("/data/flights"), 57 | columnDefs = list( 58 | list( 59 | # handle missing values: 60 | defaultContent = "", 61 | # avoid cell wrap: 62 | className = "dt-nowrap", 63 | targets = "_all" 64 | ) 65 | ), 66 | # enable horizontal scrolling: 67 | scrollX = TRUE 68 | ) 69 | ) 70 | 71 | container <- tags$div( 72 | class = "container", 73 | intro_card, 74 | table_card 75 | ) 76 | 77 | page( 78 | nav(), 79 | container 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/modules/goals/ui.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | shiny[ 3 | NS, 4 | icon, 5 | uiOutput, 6 | actionLink, 7 | actionButton, 8 | tabsetPanel, 9 | tabPanelBody, 10 | ], 11 | htmltools[tags], 12 | . / account_ui[account_ui = ui], 13 | . / dashboard_ui[dashboard_ui = ui], 14 | ) 15 | 16 | #' Create user profile btn 17 | #' 18 | #' @param ns Module namespace from which this function is called. 19 | #' @param user_name String. User name. 20 | #' @return [htmltools::tags$div()] 21 | #' @export 22 | user_profile_btn <- \(ns, user_name) { 23 | tags$div( 24 | class = "dropdown", 25 | actionButton( 26 | inputId = ns("show_account_options"), 27 | class = "dropdown-toggle", 28 | icon = icon(name = NULL, class = "fa fa-user-large"), 29 | `data-bs-toggle` = "dropdown", 30 | `aria-expanded` = "false", 31 | label = user_name 32 | ), 33 | tags$ul( 34 | class = "dropdown-menu", 35 | tags$li( 36 | actionLink( 37 | inputId = ns("logout"), 38 | label = "Logout" 39 | ) 40 | ), 41 | tags$li( 42 | actionLink( 43 | inputId = ns("go_to_account_settings"), 44 | label = "Account" 45 | ) 46 | ) 47 | ) 48 | ) 49 | } 50 | 51 | #' Goals UI module 52 | #' 53 | #' @param id String. Module id. 54 | #' @return [htmltools::tags$div()] 55 | #' @export 56 | ui <- \(id) { 57 | ns <- NS(id) 58 | 59 | header <- tags$div( 60 | class = "d-flex justify-content-between align-items-center my-4", 61 | tags$h4( 62 | class = "m-0", 63 | tags$a( 64 | class = "text-decoration-none", 65 | href = "/", 66 | "Goals" 67 | ) 68 | ), 69 | uiOutput(outputId = ns("user_profile_btn")) 70 | ) 71 | 72 | tags$div( 73 | class = "container", 74 | header, 75 | tabsetPanel( 76 | id = ns("tabs"), 77 | type = "hidden", 78 | selected = "dashboard", 79 | tabPanelBody( 80 | value = "dashboard", 81 | dashboard_ui(id = ns("dashboard")) 82 | ), 83 | tabPanelBody( 84 | value = "account", 85 | account_ui(id = ns("account")) 86 | ) 87 | ) 88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /03_basic_routing/README.md: -------------------------------------------------------------------------------- 1 | ## Basic routing 2 | 3 | **Routing** refers to determining how an application responds to 4 | a client request to a particular endpoint, which is a URI (or 5 | path) and a specific HTTP request method (GET, POST, and so on). 6 | 7 | Each route can have one or more handler functions, which are 8 | executed when the route is matched. 9 | 10 | Route definition takes the following structure: 11 | 12 | ```r 13 | app$METHOD(PATH, HANDLER) 14 | ``` 15 | 16 | - `app` is an instance of ambiorix. 17 | - `METHOD` is an [HTTP request method](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods), in lowercase. 18 | - `HANDLER` is the function executed when the route is matched. 19 | 20 | The following examples illustrate defining simple routes. 21 | 22 | Respond with `Hello World!` on the homepage: 23 | 24 | ```r 25 | app$get("/", \(req, res) { 26 | res$send("Hello World!") 27 | }) 28 | ``` 29 | 30 | Respond to POST request on the root route (`/`), the application's home page: 31 | 32 | ```r 33 | app$post("/", \(req, res) { 34 | res$send("Got a POST request") 35 | }) 36 | ``` 37 | 38 | Respond to a PUT request to the `/user` route: 39 | 40 | ```r 41 | app$put("/user", \(req, res) { 42 | res$send("Got a PUT request at /user") 43 | }) 44 | ``` 45 | 46 | Respond to a DELETE request to the `/user` route: 47 | 48 | ```r 49 | app$delete("/user", \(req, res) { 50 | res$send("Got a DELETE request at /user") 51 | }) 52 | ``` 53 | 54 | **NOTE:** 55 | - Browsers issue a GET request by default. This means that if you run the [example app](./index.R) and visit localhost 3000, you'll only be able to access the homepage (`/`). 56 | - The easiest ways to see the responses from the other routes would be to either: 57 | - Install [Postman](https://www.postman.com/) and make the requests from there (**Recommended**). The free tier is more than enough. 58 | Here is how issuing the various requests above would look like: 59 | ![GET request to /](./get-home.png) 60 | ![POST request to /](./post-home.png) 61 | ![POST request to /user](./put-user.png) 62 | ![DELETE request to /user](./delete-user.png) 63 | - Open another R session and use [httr2](https://httr2.r-lib.org/index.html) to make requests to the endpoints. 64 | -------------------------------------------------------------------------------- /15_chat/public/chat.js: -------------------------------------------------------------------------------- 1 | const icon = (() => { 2 | const x = [ 3 | "ash", 4 | "bcrikko", 5 | "bulbasaur", 6 | "charmander", 7 | "kirby", 8 | "mario", 9 | "logo", 10 | "octocat", 11 | "pokeball", 12 | "squirtle", 13 | ]; 14 | return x[Math.floor(Math.random() * x.length)]; 15 | })(); 16 | 17 | document.addEventListener("DOMContentLoaded", function() { 18 | const id = createId(); 19 | 20 | const wss = new Ambiorix(); 21 | wss.onopen(() => { 22 | console.info("Connecting"); 23 | }); 24 | 25 | wss.receive("chat", (msg) => { 26 | // it was sent by me 27 | if (msg.id == id) return; 28 | insertLeft(msg.text, msg.icon); 29 | }); 30 | 31 | wss.onclose(() => { 32 | console.error("Disconnected"); 33 | }); 34 | 35 | wss.start(); 36 | 37 | handleChat(id); 38 | }); 39 | 40 | const createId = () => { 41 | return Math.random().toString(16).slice(2); 42 | } 43 | 44 | const handleChat = (id) => { 45 | const btn = document.querySelector("#send"); 46 | btn.addEventListener("click", (_event) => { 47 | const tgt = document.querySelector("#message") 48 | const text = tgt.value; 49 | tgt.value = ""; 50 | 51 | if (!text || text == "") return; 52 | 53 | insertRight(text); 54 | Ambiorix.send("chat", { text: text, id: id, icon: icon }); 55 | }); 56 | 57 | const query = document.querySelector("#message"); 58 | query.addEventListener("keydown", (event) => { 59 | if (event.key != "Enter") return; 60 | 61 | const btn = document.querySelector("#send"); 62 | btn.click(); 63 | }) 64 | } 65 | 66 | const insertLeft = (message, icon) => { 67 | document.querySelector("#chat-list").insertAdjacentHTML("beforeend", chatLeft(message, icon)) 68 | } 69 | 70 | const insertRight = (message) => { 71 | document.querySelector("#chat-list").insertAdjacentHTML("beforeend", chatRight(message)) 72 | } 73 | 74 | const chatLeft = (message, icon) => { 75 | return `
76 | 77 |
78 |

${message}

79 |
80 |
`; 81 | } 82 | 83 | const chatRight = (message) => { 84 | return `
85 |
86 |

${message}

87 |
88 | 89 |
`; 90 | } 91 | -------------------------------------------------------------------------------- /08_datatables/store/datatable.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | htmltools[tags, tagList], 3 | jsonlite[toJSON], 4 | stringr[str_replace_all, str_to_title] 5 | ) 6 | 7 | #' Generate html to create a datatable 8 | #' 9 | #' Meant to be used for serverside rendering of [datatables](https://datatables.net/) 10 | #' 11 | #' @param col_names Column names of the data. 12 | #' @param table_id Table id. 13 | #' @param table_class Bootstrap classes to apply to the table. 14 | #' @param ... Named `key=value` pairs. Table options. Don't include 'columns'. 15 | #' See [datatable options](https://datatables.net/manual/options). 16 | #' @return An object of class `shiny.tag` 17 | #' @export 18 | datatable <- \( 19 | col_names, 20 | table_id, 21 | table_class = NULL, 22 | header_row_class = NULL, 23 | ... 24 | ) { 25 | tagList( 26 | tags$table( 27 | id = table_id, 28 | class = paste("table", table_class), 29 | tags$thead( 30 | class = header_row_class, 31 | tags$tr( 32 | lapply(col_names, \(col_name) { 33 | tags$th(scope = "col", col_name) 34 | }) 35 | ) 36 | ), 37 | tags$tbody() 38 | ), 39 | tags$script( 40 | datatable_script( 41 | id = table_id, 42 | col_names = col_names, 43 | ... 44 | ) 45 | ) 46 | ) 47 | } 48 | 49 | #' Make a script to convert a table to datatable 50 | #' 51 | #' @param id Table id. 52 | #' @param col_names Column names of the data. 53 | #' @param ... Named `key=value` pairs. Table options. Don't include 'columns'. 54 | #' See [datatable options](https://datatables.net/manual/options). 55 | #' @examples 56 | #' script <- datatable_script( 57 | #' id = "datatable", 58 | #' col_names = names(iris), 59 | #' processing = TRUE, 60 | #' serverSide = TRUE, 61 | #' ajax = create_href("/api/data") 62 | #' ) 63 | #' 64 | #' cat(script, "\n") 65 | #' @return String. JavaScript code. 66 | #' @export 67 | datatable_script <- \(id, col_names, ...) { 68 | columns <- lapply(col_names, \(name) list(data = name)) 69 | opts <- list( 70 | ..., 71 | columns = columns 72 | ) |> 73 | toJSON(auto_unbox = TRUE, pretty = TRUE) 74 | sprintf( 75 | "$(document).ready(function() { 76 | $('#%s').DataTable(%s); 77 | });", 78 | id, 79 | opts 80 | ) 81 | } 82 | 83 | #' Make user friendly names 84 | #' 85 | #' @param names Names to make user-friendly 86 | #' @return Character vector of same length as `names` 87 | #' @export 88 | make_user_friendly_names <- \(names) { 89 | names |> 90 | str_replace_all(pattern = "_", replacement = " ") |> 91 | str_to_title() 92 | } 93 | -------------------------------------------------------------------------------- /02_static_files/README.md: -------------------------------------------------------------------------------- 1 | ## Serving static files in ambiorix 2 | 3 | To serve static files such as images, CSS files, and JavaScript 4 | files, use the `app$static()` method. 5 | 6 | For example, let's say this is your directory structure: 7 | 8 | ``` 9 | |- index.R 10 | |- public/ 11 | |- index.html 12 | |- about.html 13 | |- image.jpg 14 | |- css 15 | |- styles.css 16 | |- index2.R 17 | |- main.js 18 | ``` 19 | 20 | ### `/public` 21 | 22 | To make the files accessible at the path `/public`, you'd do this: 23 | 24 | ```r 25 | app$static(path = "public", uri = "public") 26 | ``` 27 | 28 | - `path` specifies the static directory. 29 | - `uri` defines the path ambiorix should serve the static files from. 30 | 31 | So now you'll be able to do this in your app: 32 | 33 | ```r 34 | app$get("/", \(req, res) { 35 | res$send( 36 | "

Hello everyone!

37 | " 38 | ) 39 | }) 40 | ``` 41 | 42 | Also, note that you can access every static resource by navigating to it from the browser eg. [http://localhost:3000/public/image.jpg](http://localhost:3000/public/image.jpg) 43 | 44 | ### `/your-own-path` 45 | 46 | You can make static content accessible via your own custom path too. 47 | 48 | For example, let's use `/static` this time: 49 | 50 | ```r 51 | app$static(path = "public", uri = "static") 52 | ``` 53 | 54 | Then in your code you'll use this: 55 | 56 | ```r 57 | app$get("/", \(req, res) { 58 | res$send( 59 | "

Hello everyone!

60 | " 61 | ) 62 | }) 63 | ``` 64 | 65 | ## Serve regular files 66 | 67 | Take a look at: 68 | - [public/about.html](public/about.html) 69 | - [public/index.html](public/index.html) 70 | - [public/index2.R](public/index2.R) 71 | 72 | Now run the app ([index.R](index.R)) and navigate to these links in your browser: 73 | - http://localhost:3000/static/index.html 74 | - http://localhost:3000/static/about.html 75 | - http://localhost:3000/static/index2.R 76 | 77 | By making the `public/` folder static, any resource placed there can be 78 | accessed via the browser. 79 | 80 | This is usually not what you're going to use ambiorix for. For 81 | the most part you're going to either: 82 | - Build APIs so that you can connect from a frontend like React et al, OR, 83 | - Render templates where you can insert dynamic data rather than just having a static website. 84 | 85 | ## Keep this in mind: 86 | 87 | - All static files are exposed and can be accessed via the browser. DO NOT put sensitive files there. 88 | - You can also use [htmltools](https://rstudio.github.io/htmltools/index.html) tags instead of writing html strings. 89 | -------------------------------------------------------------------------------- /05_router/README.md: -------------------------------------------------------------------------------- 1 | ## Routing 2 | 3 | **Routing** refers to how an application's endpoints (URIs) respond to client requests. 4 | 5 | For an introduction to routing, see [03_basic_routing](../03_basic_routing/). 6 | 7 | If you look at [04_simple_json_api/index.R](../04_simple_json_api/index.R) you'll notice that the routes we created all belong to `/api/members` and we kept repeating that base/root route. 8 | 9 | Wouldn't it be nice to only have to use `/` and `/:id` and have ambiorix prepend the `/api/members` automatically? 10 | 11 | That would give you a better app structure making it manageable. 12 | 13 | Enter **ambiorix::Router()**. 14 | 15 | Use the `ambiorix::Router` class to create modular, mountable router handlers. 16 | 17 | A `Router` instance is a complete middleware and routing system; for this reason, it is often referred to as a "mini-app". 18 | 19 | Using the example [04_simple_json_api/index.R](../04_simple_json_api/index.R), this is how we would transform it: 20 | 21 | ```r 22 | # members.R 23 | members_router <- \() { 24 | router <- Router$new("/members") 25 | 26 | # get all members: 27 | router$get("/", \(req, res) { 28 | # ... 29 | }) 30 | 31 | # get a single member: 32 | router$get("/:id", \(req, res) { 33 | # ... 34 | }) 35 | 36 | # create a new member: 37 | router$post("/", \(req, res)) { 38 | # ... 39 | } 40 | 41 | # update member: 42 | router$put("/:id", \(req, res) { 43 | # ... 44 | }) 45 | 46 | # delete member: 47 | router$delete("/:id", \(req, res) { 48 | # ... 49 | }) 50 | 51 | router 52 | } 53 | ``` 54 | 55 | The `index.R` file would now be: 56 | 57 | ```r 58 | library(ambiorix) 59 | 60 | # 61 | 62 | PORT <- 3000 63 | 64 | app <- Ambiorix$new() 65 | 66 | # mount the router: 67 | app$use(members_router()) 68 | 69 | app$start(port = PORT, open = FALSE) 70 | ``` 71 | 72 | > [!NOTE] 73 | > Ambiorix is unopinionated. As such, it is up to you to decide how you want to bring/export the `members.R` file into `index.R`. 74 | > Some options are: 75 | > - Use [box](https://klmr.me/box/) (**Highly recommended, ⭐⭐⭐⭐⭐**) especially if you develop large apps/systems, for two reasons: 76 | > 1. Allows nested files, folders & modules (a big win) 77 | > 2. Explicit name imports ie. you're almost always sure from which package a function is from. 78 | > - Use R package structure (**Recommended, ⭐⭐⭐⭐**). Will not allow nested folders but will work really well for small to medium apps. 79 | > - `source()` files (**NOT recommended, 1⭐**). Haha. Iykyk. 80 | 81 | I provide 2 examples: 82 | - One [using box](./box/), and, 83 | - Another [using standard R package structure](./r_pkg_structure/) 84 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/store/inputs.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | shiny[ 3 | icon, 4 | textInput, 5 | passwordInput, 6 | actionButton 7 | ], 8 | htmltools[ 9 | tags, 10 | tagList, 11 | tagQuery, 12 | ] 13 | ) 14 | 15 | #' Text input 16 | #' 17 | #' @param ... Named arguments passed to [shiny::textInput()] 18 | #' @param required Logical. Whether to add the html attribute 'required' to the 19 | #' input. 20 | #' @return [htmltools::tags()] 21 | #' @export 22 | text_input <- \(..., required = TRUE) { 23 | required <- if (required) NA 24 | 25 | tag_q <- textInput(..., width = "100%") |> tagQuery() 26 | tag_q$ 27 | find("input")$ 28 | addAttrs(required = required)$ 29 | allTags() 30 | } 31 | 32 | #' Email input 33 | #' 34 | #' @param ... Named arguments passed to [shiny::textInput()] 35 | #' @param required Logical. Whether to add the html attribute 'required' to the 36 | #' input. 37 | #' @return [htmltools::tags()] 38 | #' @export 39 | email_input <- \(..., required = TRUE) { 40 | required <- if (required) NA 41 | 42 | tag_q <- textInput(..., width = "100%") |> tagQuery() 43 | tag_q$ 44 | find("input")$ 45 | removeAttrs("type")$ 46 | addAttrs(type = "email", required = required)$ 47 | allTags() 48 | } 49 | 50 | #' Password input 51 | #' 52 | #' @param ns Module namespace from which this function is called. 53 | #' @param input_id String. Input id of the password. Don't wrap in `ns()`. 54 | #' Defaults to "password". 55 | #' @param label String, [htmltools::tags()]. Label of the input. Defaults to 56 | #' "Password". 57 | #' @param required Logical. Whether to add the html attribute 'required' to the 58 | #' input. 59 | #' @return [htmltools::tags()] 60 | #' @export 61 | password_input <- \( 62 | ns, 63 | input_id = "password", 64 | label = "Password", 65 | required = TRUE 66 | ) { 67 | required <- if (required) NA 68 | input_id <- ns(input_id) 69 | 70 | btn_id <- sprintf("toggle_%s", input_id) |> ns() 71 | btn <- actionButton( 72 | inputId = btn_id, 73 | label = NULL, 74 | icon = icon(name = "eye") 75 | ) 76 | 77 | toggle_password_script <- tags$script( 78 | sprintf( 79 | "toggle_password('%s', '%s')", 80 | input_id, 81 | btn_id 82 | ) 83 | ) 84 | 85 | tag_q <- passwordInput( 86 | inputId = input_id, 87 | label = label, 88 | width = "100%" 89 | ) |> tagQuery() 90 | 91 | tag_q$find("input")$addAttrs(required = required) 92 | input <- tags$div( 93 | class = "input-group", 94 | tag_q$find("input")$selectedTags(), 95 | btn 96 | ) 97 | tag_q$find("input")$replaceWith(input) 98 | 99 | tagList( 100 | tag_q$allTags(), 101 | toggle_password_script 102 | ) 103 | } 104 | -------------------------------------------------------------------------------- /08_datatables/store/nav.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | htmltools[tags] 3 | ) 4 | 5 | #' Navbar navigation links 6 | #' 7 | #' @param href A character vector. eg. c("/", "/about" "/contact") 8 | #' @param label A character vector of same length as `href`. 9 | #' Labels for the navigation links. eg. c("Home", "About", "Contact"). 10 | #' @param active A string. The current page. One of the options from 11 | #' what you supplied to `label`. 12 | #' @details Checkout [bootstrap nav](https://getbootstrap.com/docs/5.3/components/navbar/#nav) for more info 13 | #' @examples 14 | #' \dontrun{ 15 | #' nav( 16 | #' href = c("/", "/about", "/contact"), 17 | #' label = c("Home", "About", "Contact"), 18 | #' active = "Home" 19 | #' ) 20 | #' } 21 | #' @return An object of class `shiny.tag` 22 | #' @export 23 | nav <- \( 24 | href = c("/"), 25 | label = c("Home"), 26 | active = "Home" 27 | ) { 28 | nav_items <- lapply(seq_along(href), \(i) { 29 | class <- paste("nav-link", if (label[i] == active) "active border-bottom border-dark") 30 | aria_current <- if (label[i] == active) "page" else NULL 31 | tags$li( 32 | class = "nav-item", 33 | tags$a( 34 | class = class, 35 | `aria-current` = aria_current, 36 | href = href[i], 37 | label[i] 38 | ) 39 | ) 40 | }) 41 | 42 | title <- "Axim" 43 | title_class <- "navbar-brand card-title text-uppercase fw-bold" 44 | tags$nav( 45 | class = "navbar navbar-expand-lg sticky-top mb-3 bg-white shadow-sm", 46 | tags$div( 47 | class = "container", 48 | tags$a(class = title_class, href = "/", title), 49 | tags$button( 50 | class = "navbar-toggler", 51 | type = "button", 52 | `data-bs-toggle` = "offcanvas", 53 | `data-bs-target` = "#offcanvasNavbar", 54 | `aria-controls` = "offcanvasNavbar", 55 | `aria-label` = "Toggle navigation", 56 | tags$span(class = "navbar-toggler-icon") 57 | ), 58 | tags$div( 59 | id = "offcanvasNavbar", 60 | class = "offcanvas offcanvas-end", 61 | tabindex = "-1", 62 | `aria-labelledby` = "offcanvasNavbarLabel", 63 | tags$div( 64 | class = "offcanvas-header", 65 | tags$h5( 66 | id = "offcanvasNavbarLabel", 67 | class = paste("offcanvas-title", title_class), 68 | title 69 | ), 70 | tags$button( 71 | class = "btn-close", 72 | type = "button", 73 | `data-bs-dismiss` = "offcanvas", 74 | `aria-label` = "Close" 75 | ) 76 | ), 77 | tags$div( 78 | class = "offcanvas-body", 79 | tags$ul( 80 | class = "navbar-nav justify-content-end flex-grow-1 pe-3", 81 | nav_items 82 | ) 83 | ) 84 | ) 85 | ) 86 | ) 87 | } 88 | -------------------------------------------------------------------------------- /documentation/posts/06_multi_router/index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "06: multi-router" 3 | subtitle: "Mount multiple routers" 4 | author: "Kennedy Mwavu" 5 | date: "2024-07-10" 6 | categories: [06_multi_router] 7 | --- 8 | 9 | ## Run app 10 | 11 | 1. `cd` into the `06_multi_router/` dir: 12 | 13 | ```bash 14 | cd 06_multi_router/ 15 | ``` 16 | 17 | 1. Fire up R: 18 | 19 | ```bash 20 | R 21 | ``` 22 | 23 | 1. Restore package dependencies: 24 | 25 | ```r 26 | renv::restore() 27 | ``` 28 | 29 | Once done, exit R. 30 | 1. `server.R` is the entry point. To start the app, run this on the terminal: 31 | 32 | ```bash 33 | Rscript index.R 34 | ``` 35 | 36 | ## Explanation 37 | 38 | This app starts a server and listens on port 3000 for connections. 39 | 40 | You can have multiple routers mounted onto `ambiorix::Ambiorix` via the `use()` method. 41 | 42 | In this example, I show how you can version your api. 43 | 44 | Remember, ambiorix is unopinionated. This is just my way of doing things. 45 | 46 | You'll see that using [{box}](https://github.com/klmr/box) is the easiest way to split up & 47 | organize your files & folders. 48 | 49 | This is the directory structure that I've used: 50 | 51 | ``` 52 | . 53 | ├── api 54 | │   ├── members.R 55 | │   ├── v1 56 | │   │   ├── members 57 | │   │   │   ├── controllers.R 58 | │   │   │   ├── create_new_member.R 59 | │   │   │   ├── delete_member.R 60 | │   │   │   ├── get_all_members.R 61 | │   │   │   ├── get_member_by_id.R 62 | │   │   │   └── update_member_info.R 63 | │   │   └── members.R 64 | │   └── v2 65 | │   ├── members 66 | │   │   ├── controllers.R 67 | │   │   ├── create_new_member.R 68 | │   │   ├── delete_member.R 69 | │   │   ├── get_all_members.R 70 | │   │   ├── get_member_by_id.R 71 | │   │   └── update_member_info.R 72 | │   └── members.R 73 | ├── index.R 74 | └── README.md 75 | ``` 76 | 77 | This is how `server.R` looks like: 78 | 79 | ```r 80 | box::use( 81 | ambiorix[Ambiorix], 82 | . / api / members 83 | ) 84 | 85 | Ambiorix$ 86 | new()$ 87 | listen(port = 3000L)$ 88 | use(members$v1)$ # mount API v1 members' router 89 | use(members$v2)$ # mount API v2 members' router 90 | start(open = FALSE) 91 | ``` 92 | 93 | Once you run the app, you should be able to perform requests on 94 | `http://localhost:3000/api/v*/members`, eg. 95 | 96 | - `GET` request on `http://localhost:3000/api/v1/members` 97 | - `PUT` request on `http://localhost:3000/api/v2/members/:3` 98 | 99 | ... and so on. 100 | 101 | Checkout the routers in these files: 102 | 103 | - `./api/v1/members.R` 104 | - `./api/v2/members.R` 105 | 106 | ## Dynamic rendering 107 | 108 | Are you ready for some frontend fun? See ✨[dynamic rendering](../07_dynamic_rendering/index.qmd)✨. 109 | -------------------------------------------------------------------------------- /07_dynamic_rendering/store/nav.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | htmltools[tags] 3 | ) 4 | 5 | #' Navbar navigation links 6 | #' 7 | #' @param href A character vector. eg. c("/", "/about" "/contact") 8 | #' @param label A character vector of same length as `href`. 9 | #' Labels for the navigation links. eg. c("Home", "About", "Contact"). 10 | #' @param active A string. The current page. One of the options from 11 | #' what you supplied to `label`. 12 | #' @details Checkout [bootstrap nav](https://getbootstrap.com/docs/5.3/components/navbar/#nav) for more info 13 | #' @examples 14 | #' \dontrun{ 15 | #' nav( 16 | #' href = c("/", "/about", "/contact"), 17 | #' label = c("Home", "About", "Contact"), 18 | #' active = "Home" 19 | #' ) 20 | #' } 21 | #' @return An object of class `shiny.tag` 22 | #' @export 23 | nav <- \( 24 | href = c("/", "/about", "/contact"), 25 | label = c("Home", "About", "Contact"), 26 | active = "Home" 27 | ) { 28 | nav_items <- lapply(seq_along(href), \(i) { 29 | class <- paste("nav-link", if (label[i] == active) "active border-bottom border-dark") 30 | aria_current <- if (label[i] == active) "page" else NULL 31 | tags$li( 32 | class = "nav-item", 33 | tags$a( 34 | class = class, 35 | `aria-current` = aria_current, 36 | # `data-bs-dismiss` = "offcanvas", 37 | href = href[i], 38 | label[i] 39 | ) 40 | ) 41 | }) 42 | 43 | title <- "Axim" 44 | title_class <- "navbar-brand card-title text-uppercase fw-bold" 45 | tags$nav( 46 | class = "navbar navbar-expand-lg sticky-top mb-3 bg-white shadow-sm", 47 | tags$div( 48 | class = "container", 49 | tags$a(class = title_class, href = "/", title), 50 | tags$button( 51 | class = "navbar-toggler", 52 | type = "button", 53 | `data-bs-toggle` = "offcanvas", 54 | `data-bs-target` = "#offcanvasNavbar", 55 | `aria-controls` = "offcanvasNavbar", 56 | `aria-label` = "Toggle navigation", 57 | tags$span(class = "navbar-toggler-icon") 58 | ), 59 | tags$div( 60 | id = "offcanvasNavbar", 61 | class = "offcanvas offcanvas-end", 62 | tabindex = "-1", 63 | `aria-labelledby` = "offcanvasNavbarLabel", 64 | tags$div( 65 | class = "offcanvas-header", 66 | tags$h5( 67 | id = "offcanvasNavbarLabel", 68 | class = paste("offcanvas-title", title_class), 69 | title 70 | ), 71 | tags$button( 72 | class = "btn-close", 73 | type = "button", 74 | `data-bs-dismiss` = "offcanvas", 75 | `aria-label` = "Close" 76 | ) 77 | ), 78 | tags$div( 79 | class = "offcanvas-body", 80 | tags$ul( 81 | class = "navbar-nav justify-content-end flex-grow-1 pe-3", 82 | nav_items 83 | ) 84 | ) 85 | ) 86 | ) 87 | ) 88 | } 89 | -------------------------------------------------------------------------------- /documentation/about.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "About" 3 | about: 4 | template: marquee 5 | links: 6 | - icon: github 7 | text: Github 8 | href: https://github.com/ambiorix-web/ambiorix 9 | - icon: book 10 | text: docs 11 | href: https://ambiorix.dev/ 12 | --- 13 | 14 | ## What is ambiorix? 15 | 16 | [Ambiorix](https://ambiorix.dev/) is an unopinionated and minimalist web framework for R. 17 | 18 | It is inspired by [express.js](https://expressjs.com/). In fact, 19 | the syntax is almost identical, only that it is R and not JavaScript. 20 | 21 | Ambiorix is a **server-side** or **back-end** framework. It is not 22 | comparable to client-side frameworks like React, Angular, Vue, etc. However, it can be used in combination with those frameworks 23 | to build full stack applications. 24 | 25 | ## "Unopinionated and Minimalist"... What does that mean? 26 | 27 | Ambiorix does not assume you're going to build your API (or app) in any 28 | certain way or using a certain design pattern. You have absolute 29 | full control of how you handle requests to the server and 30 | how you respond. 31 | 32 | ## So, why should I use ambiorix? 33 | 34 | - Makes building web applications with R **VERY** easy 35 | - Used for both server rendered apps as well as API/Microservices 36 | - Full control of the request and response cycle 37 | - Great to use with your favorite client side framework (whether React, Angular, Vue etc.) 38 | 39 | ## What exciting features can I look forward to? 40 | 41 | Here are the features that make ambiorix well-suited to building 42 | large systems & applications: 43 | 44 | - Out of the box **routing** 45 | - Creating a robust **API** is quick and easy 46 | - Templating (html, markdown, etc.) 47 | - Websockets in case you need bidirectional communication 48 | 49 | ## What should I know first? 50 | 51 | The main prerequisites are: 52 | 53 | - having a good understanding of **R fundamentals**, and, 54 | - basic knowledge of HTTP status codes. 55 | 56 | You can pickup most of the concepts on the go. 57 | 58 | ## Package dependencies used in examples 59 | 60 | Each example has been bootstrapped using [`{renv}`](https://rstudio.github.io/renv/articles/renv.html) to ease reproducibility. 61 | 62 | To install the dependencies for a specific example eg. `05_router`: 63 | 64 | 1. Switch to its directory 65 | 66 | ```bash 67 | cd 05_router 68 | ``` 69 | 70 | 1. Add this to the `.Renviron` file: 71 | 72 | ```r 73 | RENV_CONFIG_SANDBOX_ENABLED = FALSE 74 | ``` 75 | 76 | 1. Fire up R and restore the dependencies: 77 | 78 | ```r 79 | renv::restore() 80 | ``` 81 | 82 | ## Note 83 | 84 | In some of the examples I've committed the `.Renviron` files. I did this just to make it easier for you to run the examples. 85 | 86 | However, you should **NEVER** commit a file that has any type of credentials (`.Renviron` in this case). Such files should always be included in your `.gitignore` so that git doesn't track them. 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## What is ambiorix? 2 | 3 | [Ambiorix](https://ambiorix.dev/) is an unopinionated and minimalist web framework for R. 4 | 5 | It is inspired by [express.js](https://expressjs.com/). In fact, 6 | the syntax is almost identical, only that it is R and not JavaScript. 7 | 8 | Ambiorix is a **server-side** or **back-end** framework. It is not 9 | comparable to client-side frameworks like React, Angular, Vue, etc. However, it can be used in combination with those frameworks 10 | to build full stack applications. 11 | 12 | ## "Unopinionated and Minimalist"... What does that mean? 13 | 14 | Ambiorix does not assume you're going to build your app/API in any 15 | certain way or using a certain design pattern. You have absolute 16 | full control of how you handle requests to the server and 17 | how you respond. 18 | 19 | ## So, why should I use ambiorix? 20 | 21 | - Makes building web applications with R **VERY** easy 22 | - Used for both server rendered apps (SSRs) as well as API/Microservices 23 | - Full control of the request and response cycle 24 | - Great to use with your favorite client side framework (whether React, Angular, Vue etc.) 25 | 26 | ## What exciting features can I look forward to? 27 | 28 | Here are the features that make ambiorix well-suited to building 29 | large systems & applications: 30 | 31 | - Out of the box **routing** 32 | - Creating a robust **API** is quick and easy 33 | - Templating (HTML, markdown, pug, etc.) 34 | - Websockets in case you need bidirectional communication 35 | 36 | ## What should I know first? 37 | 38 | The main prerequisites are: 39 | 40 | - having a good understanding of **R fundamentals**, and, 41 | - basic knowledge of HTTP status codes. 42 | 43 | You can pickup most of the concepts on the go. 44 | 45 | ## Package dependencies used in examples 46 | 47 | Each example has been bootstrapped using [`{renv}`](https://rstudio.github.io/renv/articles/renv.html) to ease reproducibility. 48 | 49 | To install the dependencies for a specific example eg. `05_router`: 50 | 51 | 1. Switch to its directory 52 | 53 | ```bash 54 | cd 05_router 55 | ``` 56 | 57 | 1. Add this to the `.Renviron` file: 58 | 59 | ```r 60 | RENV_CONFIG_SANDBOX_ENABLED = FALSE 61 | ``` 62 | 63 | 1. Fire up R and restore the dependencies: 64 | 65 | ```r 66 | renv::restore() 67 | ``` 68 | 69 | ## Documentation 70 | 71 | You can find documentation on each of the examples at [ambiorix-web/ambiorix-examples](https://ambiorix-web.github.io/ambiorix-examples/). 72 | 73 | The documentation includes: 74 | 75 | - how to run each example 76 | - code and concept explanations where necessary 77 | 78 | ## Note 79 | 80 | In some of the examples I've committed the `.Renviron` files. I did this just to make it easier for you to run the examples. 81 | 82 | However, you should **NEVER** commit a file that has any type of credentials (`.Renviron` in this case). Such files should always be included in your `.gitignore` so that git doesn't track them. 83 | -------------------------------------------------------------------------------- /documentation/posts/02_static_files/index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "02: static files" 3 | subtitle: "Serve static files" 4 | author: "Kennedy Mwavu" 5 | date: "2024-07-10" 6 | categories: [02_static_files] 7 | --- 8 | 9 | ## Run app 10 | 11 | 1. `cd` into the `02_static_files/` dir: 12 | 13 | ```bash 14 | cd 02_static_files/ 15 | ``` 16 | 17 | 1. Fire up R: 18 | 19 | ```bash 20 | R 21 | ``` 22 | 23 | 1. Restore package dependencies: 24 | 25 | ```r 26 | renv::restore() 27 | ``` 28 | 29 | Once done, exit R. 30 | 1. `index.R` is the entry point. To start the app, run this on the terminal: 31 | 32 | ```bash 33 | Rscript index.R 34 | ``` 35 | 36 | ## Explanation 37 | 38 | This app starts a server and listens on port 3000 for connections. 39 | 40 | It has a single endpoint: 41 | 42 | - `/`: [localhost:3000/](http://localhost:3000/) 43 | 44 | Now that the app is running, navigate to these links in your browser: 45 | 46 | - [localhost:3000/static/index.html](http://localhost:3000/static/index.html) 47 | - [localhost:3000/static/about.html](http://localhost:3000/static/about.html) 48 | - [localhost:3000/static/index2.R](http://localhost:3000/static/index2.R) 49 | 50 | To serve static files such as images, CSS files, JavaScript 51 | files etc., use the `app$static()` method. 52 | 53 | For example, let's say this is your directory structure: 54 | 55 | ``` 56 | |- index.R 57 | |- public/ 58 | |- index.html 59 | |- about.html 60 | |- image.jpg 61 | |- css 62 | |- styles.css 63 | |- index2.R 64 | |- main.js 65 | ``` 66 | 67 | ### `/public` 68 | 69 | To make the files accessible at the path `/public`, you'd do this: 70 | 71 | ```r 72 | app$static(path = "public", uri = "public") 73 | ``` 74 | 75 | - `path` specifies the static directory. 76 | - `uri` defines the path ambiorix should serve the static files from. 77 | 78 | So now you'll be able to do this in your app: 79 | 80 | ```r 81 | app$get("/", \(req, res) { 82 | res$send( 83 | "

Hello everyone!

84 | " 85 | ) 86 | }) 87 | ``` 88 | 89 | By making the `public/` folder static, any resource placed there can be 90 | accessed via the browser eg. http://localhost:3000/public/image.jpg 91 | 92 | ### `/your-own-path` 93 | 94 | You can make static content accessible via your own custom path too. 95 | 96 | For example, let's use `/static` this time: 97 | 98 | ```r 99 | app$static(path = "public", uri = "static") 100 | ``` 101 | 102 | Then in your code you'll use this: 103 | 104 | ```r 105 | app$get("/", \(req, res) { 106 | res$send( 107 | "

Hello everyone!

108 | " 109 | ) 110 | }) 111 | ``` 112 | 113 | ## Keep this in mind: 114 | 115 | - All static files are exposed and can be accessed via the browser. DO NOT put sensitive files there. 116 | - You can also use [htmltools](https://rstudio.github.io/htmltools/index.html) tags instead of writing html strings. 117 | 118 | ## Basic routing 119 | 120 | Learn the ✨[basics of routing](../03_basic_routing/index.qmd)✨. 121 | -------------------------------------------------------------------------------- /documentation/posts/03_basic_routing/index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "03: basic routing" 3 | subtitle: "An intro to routes" 4 | author: "Kennedy Mwavu" 5 | date: "2024-07-10" 6 | categories: [03_basic_routing] 7 | --- 8 | 9 | ## Run app 10 | 11 | 1. `cd` into the `03_basic_routing/` dir: 12 | 13 | ```bash 14 | cd 03_basic_routing/ 15 | ``` 16 | 17 | 1. Fire up R: 18 | 19 | ```bash 20 | R 21 | ``` 22 | 23 | 1. Restore package dependencies: 24 | 25 | ```r 26 | renv::restore() 27 | ``` 28 | 29 | Once done, exit R. 30 | 1. `index.R` is the entry point. To start the app, run this on the terminal: 31 | 32 | ```bash 33 | Rscript index.R 34 | ``` 35 | 36 | ## Explanation 37 | 38 | This app starts a server and listens on port 3000 for connections. 39 | 40 | It has two endpoints: 41 | 42 | - `/` 43 | - `/user` 44 | 45 | **Routing** refers to determining how an application responds to 46 | a client request to a particular endpoint, which is a URI (or 47 | path) and a specific HTTP request method (GET, POST, and so on). 48 | 49 | Each route can have one or more handler functions, which are 50 | executed when the route is matched. 51 | 52 | Route definition takes the following structure: 53 | 54 | ```r 55 | app$METHOD(PATH, HANDLER) 56 | ``` 57 | 58 | - `app` is an instance of ambiorix. 59 | - `METHOD` is an [HTTP request method](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods), in lowercase. 60 | - `HANDLER` is the function executed when the route is matched. 61 | 62 | The following examples illustrate defining simple routes. 63 | 64 | Respond with `Hello World!` on the homepage: 65 | 66 | ```r 67 | app$get("/", \(req, res) { 68 | res$send("Hello World!") 69 | }) 70 | ``` 71 | 72 | Respond to POST request on the root route (`/`), the application's home page: 73 | 74 | ```r 75 | app$post("/", \(req, res) { 76 | res$send("Got a POST request") 77 | }) 78 | ``` 79 | 80 | Respond to a PUT request to the `/user` route: 81 | 82 | ```r 83 | app$put("/user", \(req, res) { 84 | res$send("Got a PUT request at /user") 85 | }) 86 | ``` 87 | 88 | Respond to a DELETE request to the `/user` route: 89 | 90 | ```r 91 | app$delete("/user", \(req, res) { 92 | res$send("Got a DELETE request at /user") 93 | }) 94 | ``` 95 | 96 | ## Keep this in mind: 97 | 98 | - Browsers issue a GET request by default. This means that once you run this example and visit localhost 3000, you'll only be able to access the homepage (`/`). 99 | - The easiest way to see the responses from the other routes would be to either: 100 | - Install [Postman](https://www.postman.com/) and make the requests from there (**Recommended**). The free tier is more than enough. 101 | Here is how issuing the various requests above would look like: 102 | ![GET request to /](./get-home.png) 103 | ![POST request to /](./post-home.png) 104 | ![POST request to /user](./put-user.png) 105 | ![DELETE request to /user](./delete-user.png) 106 | - Open another R session and use [httr2](https://httr2.r-lib.org/index.html) to make requests to the endpoints. 107 | 108 | ## A simple JSON API 109 | 110 | You're now ready to build ✨[a simple JSON API](../04_simple_json_api/index.qmd)✨. -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/modules/auth/proxy.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | httr2[ 3 | request, 4 | req_method, 5 | req_perform, 6 | req_url_path, 7 | last_response, 8 | resp_body_json, 9 | req_body_multipart, 10 | req_auth_bearer_token, 11 | ], 12 | .. / .. / helpers / mod[get_base_url], 13 | .. / .. / store / mod[toast_nofitication], 14 | ) 15 | 16 | #' Create user account 17 | #' 18 | #' @param name String. Name of user. 19 | #' @param email String. User email. 20 | #' @param password String. Password. 21 | #' @return Named list. 22 | #' @export 23 | create_account <- \(name, email, password) { 24 | user_details <- list( 25 | name = name, 26 | email = email, 27 | password = password 28 | ) 29 | 30 | request(base_url = get_base_url()) |> 31 | req_url_path("/api/users") |> 32 | req_body_multipart(!!!user_details) |> 33 | req_perform() |> 34 | resp_body_json() 35 | } 36 | 37 | #' Login user 38 | #' 39 | #' @param email String. User email. 40 | #' @param password String. Password. 41 | #' @return Named list. 42 | #' @export 43 | login <- \(email, password) { 44 | user_details <- list( 45 | email = email, 46 | password = password 47 | ) 48 | 49 | request(base_url = get_base_url()) |> 50 | req_url_path("/api/users/login") |> 51 | req_body_multipart(!!!user_details) |> 52 | req_perform() |> 53 | resp_body_json() 54 | } 55 | 56 | #' Get user account details 57 | #' 58 | #' @param token String. JWT token. 59 | #' @return Named list. 60 | #' @export 61 | get_account_details <- \(token) { 62 | request(base_url = get_base_url()) |> 63 | req_url_path("/api/users/me") |> 64 | req_auth_bearer_token(token = token) |> 65 | req_perform() |> 66 | resp_body_json() 67 | } 68 | 69 | #' Update user account details 70 | #' 71 | #' @param name String. Name of user. 72 | #' @param email String. User email. 73 | #' @param password String. Password. 74 | #' @param token String. JWT token. 75 | #' @return Named list. 76 | #' @export 77 | update_account_details <- \( 78 | name = NULL, 79 | email = NULL, 80 | password = NULL, 81 | token 82 | ) { 83 | new_details <- list( 84 | name = name, 85 | email = email, 86 | password = password 87 | ) 88 | 89 | request(base_url = get_base_url()) |> 90 | req_url_path("/api/users/me") |> 91 | req_auth_bearer_token(token = token) |> 92 | req_method(method = "PUT") |> 93 | req_body_multipart(!!!new_details) |> 94 | req_perform() |> 95 | resp_body_json() 96 | } 97 | 98 | #' Delete user account 99 | #' 100 | #' @param token String. JWT token. 101 | #' @return Named list. 102 | #' @export 103 | delete_account <- \(token) { 104 | request(base_url = get_base_url()) |> 105 | req_url_path("/api/users/me") |> 106 | req_auth_bearer_token(token = token) |> 107 | req_method(method = "DELETE") |> 108 | req_perform() |> 109 | resp_body_json() 110 | } 111 | 112 | #' Request error handler 113 | #' 114 | #' @param e Error object. See [stop()]. 115 | #' @export 116 | req_error_handler <- \(e) { 117 | error <- last_response() |> resp_body_json() 118 | print(error) 119 | toast_nofitication( 120 | title = "Error!", 121 | message = error$msg, 122 | type = "error" 123 | ) 124 | } 125 | -------------------------------------------------------------------------------- /documentation/posts/14_cors/index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "14: CORS" 3 | subtitle: "how to allow CORS in ambiorix" 4 | author: "Kennedy Mwavu" 5 | date: "2024-08-24" 6 | categories: [CORS] 7 | --- 8 | 9 | You're probably reading this because you've hit that CORS error in your browser :) 10 | 11 | TL;DR: See the [middleware](https://github.com/ambiorix-web/ambiorix-examples/tree/main/14_cors). 12 | 13 | # Quoting [mdn web docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS): 14 | 15 | **Cross-Origin Resource Sharing** (CORS) is an HTTP-header based mechanism that 16 | allows a server to indicate any origins (domain, scheme, or port) than its own 17 | from which a browser should permit loading resources. 18 | 19 | CORS also relies on a mechanism by which browsers make a "preflight" request 20 | to the server hosting the cross-origin resource, in order to check that the 21 | server will permit the actual request. In that preflight, the browser sends 22 | the headers that indicate the HTTP method and headers that will be used in the 23 | actual request. 24 | 25 | An example of cross-origin request: the front-end JavaScript code served from 26 | `https://domain-a.com` uses `fetch()` to make a request for `https://domain-b.com/data.json`. 27 | 28 | For security reasons, browsers restrict cross-origin HTTP requests initiated 29 | from scripts. 30 | 31 | For example, `fetch()` and `XMLHtttpRequest` follow the 'same-origin policy'. 32 | This means that a web application using those APIs can only request resources 33 | from the same origin the application was loaded from unless the response 34 | from the other origins includes the right CORS headers. 35 | 36 | # How to allow CORS 37 | 38 | It's really simple. This image from [html5rocks.com](https://www.html5rocks.com/static/images/cors_server_flowchart.png) is all you need: 39 | 40 | ![](./cors_server_flowchart.png) 41 | 42 | # Middleware 43 | 44 | As [Rihanna and Calvin Harris](https://www.youtube.com/watch?v=kOkQ4T5WO9E) would tell you, This Is What You Came For: 45 | 46 | ```r 47 | #' Allow CORS 48 | #' 49 | #' @details 50 | #' Sets these headers in the response: 51 | #' - `Access-Control-Allow-Methods` 52 | #' - `Access-Control-Allow-Headers` 53 | #' - `Access-Control-Allow-Origin` 54 | #' @export 55 | cors <- \(req, res) { 56 | res$header("Access-Control-Allow-Origin", "http://127.0.0.1:8000") 57 | 58 | if (req$REQUEST_METHOD == "OPTIONS") { 59 | res$header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE") 60 | res$header( 61 | "Access-Control-Allow-Headers", 62 | req$HEADERS$`access-control-request-headers` 63 | ) 64 | 65 | return( 66 | res$set_status(200L)$send("") 67 | ) 68 | } 69 | } 70 | ``` 71 | 72 | Change the values set for the headers to suit your specific use-case. 73 | 74 | If your API requires the use of cookies or authentication tokens across 75 | domains, you will also need to set the "Access-Control-Allow-Credentials" 76 | header inside the `if () {}` block: 77 | 78 | ```r 79 | res$header("Access-Control-Allow-Credentials", "true") 80 | ``` 81 | 82 | # Note 83 | 84 | - DO NOT include a trailing slash in the allowed origins. 85 | - `http://127.0.0.1:8000/`❌ 86 | - `http://127.0.0.1:8000`✅ 87 | - In Ambiorix, middleware is executed in the order it is registered with 88 | `use()`. Make sure this middleware is the first one in the sequence. 89 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/modules/goals/dashboard_ui.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | shiny[ 3 | NS, 4 | uiOutput, 5 | textInput, 6 | modalDialog, 7 | actionButton, 8 | ], 9 | htmltools[tags], 10 | reactable[reactableOutput], 11 | .. / .. / store / mod[ 12 | text_input, 13 | center_modal, 14 | ], 15 | ) 16 | 17 | #' Create new goal UI 18 | #' 19 | #' @param ns Module namespace from which this function is called. 20 | #' @return [htmltools::tags$div()] 21 | #' @export 22 | new_goal_ui <- \(ns) { 23 | tags$form( 24 | id = ns("new_goal_form"), 25 | tags$div( 26 | class = "d-flex", 27 | text_input( 28 | inputId = ns("new_goal"), 29 | label = NULL, 30 | placeholder = "Enter goal here" 31 | ), 32 | actionButton( 33 | inputId = ns("create"), 34 | class = "ms-1 mb-3", 35 | label = "Create", 36 | type = "submit" 37 | ) 38 | ) 39 | ) 40 | } 41 | 42 | #' Goal modal UI 43 | #' 44 | #' Can be used as both the edit or delete goal modal. 45 | #' @param ns Module namespace from which this function is called. 46 | #' @param type String. Type of modal. Either "edit" (default) or "delete". 47 | #' @param text String. Current text value of goal. Defaults to `NULL`. 48 | #' @return [shiny::modalDialog()] 49 | #' @export 50 | goal_modal <- \( 51 | ns, 52 | type = c("edit", "delete"), 53 | text = NULL 54 | ) { 55 | type <- match.arg(arg = type) 56 | is_edit <- identical(type, "edit") 57 | cancel_id <- paste0("cancel_", type) 58 | confirm_id <- paste0("confirm_", type) 59 | confirm_label <- if (is_edit) "Save" else "Confirm" 60 | title <- paste( 61 | if (is_edit) "Edit" else "Delete", 62 | "goal" 63 | ) 64 | 65 | modalDialog( 66 | title = NULL, 67 | footer = NULL, 68 | size = "m", 69 | easyClose = TRUE, 70 | tags$h5( 71 | class = "mb-3", 72 | title 73 | ), 74 | if (is_edit) { 75 | textInput( 76 | inputId = ns("edited_goal"), 77 | label = NULL, 78 | value = text, 79 | width = "100%" 80 | ) 81 | }, 82 | tags$div( 83 | class = "d-flex justify-content-between", 84 | actionButton( 85 | inputId = ns(cancel_id), 86 | label = "Cancel" 87 | ), 88 | actionButton( 89 | inputId = ns(confirm_id), 90 | label = confirm_label 91 | ) 92 | ) 93 | ) |> 94 | center_modal() 95 | } 96 | 97 | #' View goals UI 98 | #' 99 | #' @param ns Module namespace from which this function is called. 100 | #' @return [htmltools::tags$div()] 101 | #' @export 102 | view_goals_ui <- \(ns) { 103 | tags$div( 104 | reactableOutput(outputId = ns("goals")), 105 | tags$div( 106 | id = ns("btn_container"), 107 | class = "d-none justify-content-between my-2", 108 | actionButton( 109 | inputId = ns("edit"), 110 | label = "Edit" 111 | ), 112 | actionButton( 113 | inputId = ns("delete"), 114 | label = "Delete" 115 | ) 116 | ) 117 | ) 118 | } 119 | 120 | #' Dashboard UI module 121 | #' 122 | #' @param id String. Module id. 123 | #' @return [htmltools::tags$div()] 124 | #' @export 125 | ui <- \(id) { 126 | ns <- NS(id) 127 | 128 | tags$div( 129 | new_goal_ui(ns = ns), 130 | view_goals_ui(ns = ns) 131 | ) 132 | } 133 | -------------------------------------------------------------------------------- /documentation/posts/05_router/index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "05: router" 3 | subtitle: "What's a router?" 4 | author: "Kennedy Mwavu" 5 | date: "2024-07-10" 6 | categories: [05_router] 7 | --- 8 | 9 | ## Run app 10 | 11 | 1. `cd` into the `05_router/` dir: 12 | 13 | ```bash 14 | cd 05_router/ 15 | ``` 16 | 17 | - There are 2 directories there: `box/` & `r_pkg_structure/`. 18 | - `cd` into any of them, say, `r_pkg_structure/`: 19 | 20 | ```bash 21 | cd r_pkg_structure/ 22 | ``` 23 | 24 | 1. Fire up R: 25 | 26 | ```bash 27 | R 28 | ``` 29 | 30 | 1. Restore package dependencies: 31 | 32 | ```r 33 | renv::restore() 34 | ``` 35 | 36 | Once done, exit R. 37 | 1. `server.R` is the entry point. To start the app, run this on the terminal: 38 | 39 | ```bash 40 | Rscript index.R 41 | ``` 42 | 43 | ## Explanation 44 | 45 | This app starts a server and listens on port 3000 for connections. 46 | 47 | It has two endpoints: 48 | 49 | - `/api/members` 50 | - `/api/members/:id` 51 | 52 | **Routing** refers to how an application's endpoints (URIs) respond to client requests. 53 | 54 | For an introduction to routing, see [03_basic_routing](../03_basic_routing/index.qmd). 55 | 56 | If you look at [04_simple_json_api](../04_simple_json_api/index.qmd) you'll notice that the routes we created all belong to `/api/members` and we kept repeating that base/root route. 57 | 58 | Wouldn't it be nice to only have to use `/` and `/:id` and have ambiorix prepend the `/api/members` automatically? 59 | 60 | That would give you a better app structure making it manageable. 61 | 62 | ## Enter `ambiorix::Router()`. 63 | 64 | Use the `ambiorix::Router` class to create modular, mountable router handlers. 65 | 66 | A `Router` instance is a complete middleware and routing system; for this reason, it is often referred to as a "mini-app". 67 | 68 | Using the example [04_simple_json_api](../04_simple_json_api/index.qmd), this is how we would transform it: 69 | 70 | ```r 71 | # members.R 72 | members_router <- \() { 73 | router <- Router$new("/members") 74 | 75 | # get all members: 76 | router$get("/", \(req, res) { 77 | # ... 78 | }) 79 | 80 | # get a single member: 81 | router$get("/:id", \(req, res) { 82 | # ... 83 | }) 84 | 85 | # create a new member: 86 | router$post("/", \(req, res)) { 87 | # ... 88 | } 89 | 90 | # update member: 91 | router$put("/:id", \(req, res) { 92 | # ... 93 | }) 94 | 95 | # delete member: 96 | router$delete("/:id", \(req, res) { 97 | # ... 98 | }) 99 | 100 | router 101 | } 102 | ``` 103 | 104 | The `server.R` file would now be: 105 | 106 | ```r 107 | library(ambiorix) 108 | 109 | # 110 | 111 | PORT <- 3000 112 | 113 | app <- Ambiorix$new() 114 | 115 | # mount the router: 116 | app$use(members_router()) 117 | 118 | app$start(port = PORT, open = FALSE) 119 | ``` 120 | 121 | ## Keep this in mind: 122 | 123 | Ambiorix is unopinionated. As such, it is up to you to decide how you want to bring/export the `members.R` file into `index.R`. 124 | 125 | Some options are: 126 | 127 | - Use [box](https://klmr.me/box/) (**Highly recommended, ⭐⭐⭐⭐⭐**) especially if you develop large apps/systems, for two reasons: 128 | 1. Allows nested files, folders & modules (a big win) 129 | 2. Explicit name imports ie. you're almost always sure from which package a function is from. 130 | - Use R package structure (**Recommended, ⭐⭐⭐⭐**). Will not allow nested folders but will work really well for small to medium apps. 131 | - `source()` files (**NOT recommended, 1⭐**). Haha. Iykyk. 132 | 133 | Choose wisely. 134 | 135 | ## Multiple routers 136 | 137 | Learn how you can mount ✨[multiple routers](../06_multi_router/index.qmd)✨. 138 | 139 | -------------------------------------------------------------------------------- /09_goals/controllers/goal_controller.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | ambiorix[parse_multipart], 3 | .. / config / db[goals_conn], 4 | .. / helpers / mongo_query[mongo_query], 5 | .. / helpers / to_json[to_json], 6 | .. / helpers / insert[insert] 7 | ) 8 | 9 | #' Get goals 10 | #' 11 | #' GET `/api/goals`. Private access. 12 | #' @export 13 | get_goals <- \(req, res) { 14 | # remember we set the user on the req object at `protect()`: 15 | goals <- goals_conn$find( 16 | query = mongo_query(user_id = req$user$`_id`), 17 | fields = mongo_query("_id" = TRUE, text = TRUE) 18 | ) 19 | 20 | response <- list(goals = goals) 21 | 22 | res$json(response) 23 | } 24 | 25 | #' Set a goal 26 | #' 27 | #' POST `/api/goals`. Private access. 28 | #' @export 29 | set_goal <- \(req, res) { 30 | body <- parse_multipart(req) 31 | text <- body$text 32 | 33 | if (is.null(text)) { 34 | response <- list( 35 | code = 400L, 36 | msg = "Please add a 'text' field in the body" 37 | ) 38 | return( 39 | res$set_status(400L)$json(response) 40 | ) 41 | } 42 | 43 | goal <- data.frame(user_id = req$user$`_id`, text = text) 44 | doc <- insert(conn = goals_conn, data = goal) 45 | response <- list( 46 | code = 201L, 47 | msg = "Success.", 48 | goal = as.list(doc) 49 | ) 50 | 51 | res$set_status(201L)$json(response) 52 | } 53 | 54 | #' Update goal 55 | #' 56 | #' PUT `/api/goals/:id`. Private access. 57 | #' @export 58 | update_goal <- \(req, res) { 59 | id <- req$params$id 60 | goal <- tryCatch( 61 | expr = goals_conn$find( 62 | query = mongo_query( 63 | user_id = req$user$`_id`, 64 | "_id" = list("$oid" = id) 65 | ), 66 | fields = mongo_query("_id" = TRUE, text = TRUE) 67 | ), 68 | error = \(e) { 69 | print(e) 70 | data.frame() 71 | } 72 | ) 73 | 74 | if (nrow(goal) == 0) { 75 | response <- list( 76 | code = 400L, 77 | msg = "Goal not found" 78 | ) 79 | return( 80 | res$set_status(400L)$json(response) 81 | ) 82 | } 83 | 84 | body <- parse_multipart(req) 85 | text <- body$text 86 | 87 | goals_conn$update( 88 | query = mongo_query( 89 | user_id = req$user$`_id`, 90 | "_id" = list("$oid" = id) 91 | ), 92 | update = mongo_query( 93 | "$set" = list(text = text) 94 | ) 95 | ) 96 | 97 | updated_goal <- goals_conn$find( 98 | query = mongo_query( 99 | user_id = req$user$`_id`, 100 | "_id" = list("$oid" = id) 101 | ), 102 | fields = mongo_query("_id" = TRUE, text = TRUE) 103 | ) 104 | 105 | response <- list( 106 | code = 200L, 107 | msg = "Goal updated successfully", 108 | goal = as.list(updated_goal) 109 | ) 110 | res$json(response) 111 | } 112 | 113 | #' Delete goal 114 | #' 115 | #' DELETE `/api/goals/:id`. Private access. 116 | #' @export 117 | delete_goal <- \(req, res) { 118 | id <- req$params$id 119 | goal <- tryCatch( 120 | expr = goals_conn$find( 121 | query = mongo_query( 122 | user_id = req$user$`_id`, 123 | "_id" = list("$oid" = id) 124 | ), 125 | fields = mongo_query("_id" = TRUE, text = TRUE) 126 | ), 127 | error = \(e) { 128 | print(e) 129 | data.frame() 130 | } 131 | ) 132 | 133 | if (nrow(goal) == 0) { 134 | msg <- list( 135 | code = 400L, 136 | msg = "Goal not found" 137 | ) 138 | return( 139 | res$set_status(400L)$json(msg) 140 | ) 141 | } 142 | 143 | goals_conn$remove( 144 | query = mongo_query( 145 | user_id = req$user$`_id`, 146 | "_id" = list("$oid" = id) 147 | ) 148 | ) 149 | 150 | response <- list( 151 | code = 200L, 152 | msg = "Goal deleted successfully", 153 | goal = as.list(goal) 154 | ) 155 | 156 | res$json(response) 157 | } 158 | -------------------------------------------------------------------------------- /10_live_reloading/README.md: -------------------------------------------------------------------------------- 1 | ## Live Reloading 2 | 3 | When building applications (whether backend or frontend), it gets tiring to manually stop & restart the app when you make changes to the source code. 4 | 5 | This guide shows you how you can monitor files for changes and have the app restart automatically. 6 | 7 | > [!NOTE] 8 | > This is not something specific to ambiorix, but it is extremely useful during my development process, so I saw it a good idea to have it included as part of the examples. 9 | 10 | ## Prerequisites 11 | 12 | - A working installation of [nodejs](https://nodejs.org/en) & [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) 13 | 14 | In case you'd like to know, we won't be writing any JavaScript. 15 | 16 | ## Setup 17 | 18 | 1. You of course need to have an app. You can use any of the code in the previous examples. We will use a simple `server.R` file, which is also the entrypoint: 19 | ```R 20 | library(ambiorix) 21 | 22 | app <- Ambiorix$new() 23 | 24 | app$get("/", \(req, res){ 25 | res$send("Using {ambiorix}!") 26 | }) 27 | 28 | app$get("/about", \(req, res){ 29 | res$text("About") 30 | }) 31 | 32 | app$start() 33 | ``` 34 | 35 | 2. Change to your project's root dir. 36 | 37 | ```bash 38 | cd myproject 39 | ``` 40 | 3. Initialize an npm project & create the `package.json` file: 41 | 42 | ```bash 43 | npm init -y 44 | ``` 45 | The `-y` flag accepts the default npm setup. 46 | 4. Install [`nodemon`](https://www.npmjs.com/package/nodemon) as a dev dependency: 47 | ```bash 48 | npm i -D nodemon 49 | ``` 50 | 1. Create the file `nodemon.json` at the root dir of your project and paste this in it: 51 | ```bash 52 | { 53 | "execMap": { 54 | "R": "Rscript" 55 | }, 56 | "ext": "*" 57 | } 58 | ``` 59 | This specifies an executable mapping for `.R` files: `Rscript`. 60 | 61 | It also tells nodemon to monitor all files (`*`) for changes. 62 | You can as well monitor specific file extensions. For example to only watch `.R`, `.html`, `.css` & `.js` files, change `ext` to: 63 | 64 | ```bash 65 | "ext": "R,html,css,js" 66 | ``` 67 | 1. Open `package.json` and edit the "scripts" section to this: 68 | ```bash 69 | "scripts": { 70 | "dev": "nodemon --signal SIGTERM server.R" 71 | } 72 | ``` 73 | This tells nodemon to re-run the file `server.R` when changes happen. 74 | 75 | The `--signal SIGTERM` is basically telling nodemon to send a termination signal to the previously running program before spawning a new one. This is especially useful for freeing the port the app is running on, and then re-using it again. 76 | 1. Run the app: 77 | ```bash 78 | npm run dev 79 | ``` 80 | This runs the `dev` script which starts, stops & restarts your app when changes occurs. 81 | 82 | Now try making some changes to your source code and enjoy the experience. 83 | 84 | 2. When working on the backend, you don't want a browser tab to open each time the app is restarted since you will mostly be sending requests via postman, so you will set `start(open = FALSE)` in your `server.R`: 85 | ```R 86 | library(ambiorix) 87 | 88 | app <- Ambiorix$new() 89 | 90 | app$get("/", \(req, res){ 91 | res$send("Using {ambiorix}!") 92 | }) 93 | 94 | app$get("/about", \(req, res){ 95 | res$text("About") 96 | }) 97 | 98 | app$start(open = FALSE) 99 | ``` 100 | 1. To stop npm, press `CTRL` + `C`. 101 | 1. Add `node_modules/` to your `.gitignore` file. 102 | 2. You can as well add `nodemon.json`, `package-lock.json`, & `package.json` to `.gitignore` since they're just used for development purposes. I have commited them for this example just so you can see their contents. 103 | -------------------------------------------------------------------------------- /12_shiny_frontend_for_09_goals/modules/goals/account_ui.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | shiny[ 3 | NS, 4 | tagList, 5 | uiOutput, 6 | textInput, 7 | modalDialog, 8 | actionButton, 9 | ], 10 | htmltools[tags], 11 | reactable[reactableOutput], 12 | .. / .. / store / mod[ 13 | center_modal, 14 | text_input, 15 | email_input, 16 | password_input, 17 | ], 18 | ) 19 | 20 | #' Edit user field modal UI 21 | #' 22 | #' @param ns Module namespace from which this function is called. 23 | #' @param field String. The field to edit. Either "name" (default), 24 | #' "email" or "password". 25 | #' @param text String. Current text value of the field. Defaults 26 | #' to `NULL`. 27 | #' @return [shiny::modalDialog()] 28 | #' @export 29 | edit_field_modal <- \( 30 | ns, 31 | field = c("name", "email", "password"), 32 | text = NULL 33 | ) { 34 | field <- match.arg(arg = field) 35 | prev_id <- paste0("prev_", field) 36 | new_id <- paste0("new_", field) 37 | label <- paste("New", field) 38 | 39 | inputs <- switch( 40 | EXPR = field, 41 | password = tagList( 42 | password_input( 43 | ns = ns, 44 | input_id = prev_id, 45 | label = "Current password" 46 | ), 47 | password_input( 48 | ns = ns, 49 | input_id = new_id, 50 | label = label 51 | ) 52 | ), 53 | email = email_input( 54 | inputId = ns(new_id), 55 | label = label, 56 | value = text 57 | ), 58 | name = text_input( 59 | inputId = ns(new_id), 60 | label = label, 61 | value = text 62 | ) 63 | ) 64 | 65 | btns <- tags$div( 66 | class = "d-flex justify-content-between", 67 | actionButton( 68 | inputId = ns("cancel_edit"), 69 | label = "Cancel" 70 | ), 71 | actionButton( 72 | inputId = ns("confirm_edit"), 73 | label = "Save" 74 | ) 75 | ) 76 | 77 | modalDialog( 78 | title = NULL, 79 | footer = NULL, 80 | size = "m", 81 | easyClose = TRUE, 82 | inputs, 83 | btns 84 | ) |> 85 | center_modal() 86 | } 87 | 88 | #' Delete account modal UI 89 | #' 90 | #' @param ns Module namespace from which this function is called. 91 | #' @return [shiny::modalDialog()] 92 | #' @export 93 | delete_account_modal <- \(ns) { 94 | btns <- tags$div( 95 | class = "d-flex justify-content-between", 96 | actionButton( 97 | inputId = ns("cancel_deletion"), 98 | label = "Cancel" 99 | ), 100 | actionButton( 101 | inputId = ns("confirm_deletion"), 102 | label = "Confirm" 103 | ) 104 | ) 105 | 106 | modalDialog( 107 | title = NULL, 108 | footer = NULL, 109 | size = "m", 110 | easyClose = TRUE, 111 | tags$h5("Delete account"), 112 | tags$p("Are you sure you want to delete your account?"), 113 | btns 114 | ) |> 115 | center_modal() 116 | } 117 | 118 | #' View account details UI 119 | #' 120 | #' @param ns Module namespace from which this function is called. 121 | #' @return [htmltools::tags$div()] 122 | #' @export 123 | view_account_ui <- \(ns) { 124 | tags$div( 125 | tags$h5("Edit account details"), 126 | reactableOutput(outputId = ns("account_details")), 127 | tags$div( 128 | id = ns("btn_container"), 129 | class = "d-none justify-content-between my-2", 130 | actionButton( 131 | inputId = ns("edit"), 132 | label = "Edit" 133 | ) 134 | ), 135 | tags$hr(), 136 | tags$h5("Delete account"), 137 | actionButton( 138 | inputId = ns("delete"), 139 | label = "Delete" 140 | ), 141 | tags$hr(), 142 | actionButton( 143 | inputId = ns("go_back_to_dashboard"), 144 | label = "Go back" 145 | ) 146 | ) 147 | } 148 | 149 | #' Account UI module 150 | #' 151 | #' @param id String. Module id. 152 | #' @return [htmltools::tagList()] 153 | #' @export 154 | ui <- \(id) { 155 | ns <- NS(id) 156 | 157 | tagList( 158 | view_account_ui(ns = ns) 159 | ) 160 | } 161 | -------------------------------------------------------------------------------- /07_dynamic_rendering/store/contact.R: -------------------------------------------------------------------------------- 1 | box::use( 2 | htmltools[tags, tagList], 3 | . / nav[nav], 4 | . / create_card[create_card] 5 | ) 6 | 7 | #' The "Contact" page 8 | #' 9 | #' @return An object of class `shiny.tag` 10 | #' @export 11 | contact <- \() { 12 | tagList( 13 | nav(active = "Contact"), 14 | tags$div( 15 | class = "container", 16 | create_card( 17 | title = "Talk to us", 18 | title_icon = tags$i(class = "bi bi-chat-dots"), 19 | title_class = "text-primary", 20 | class = "shadow-sm", 21 | tags$p( 22 | "Let us know what you think by sending us a message below" 23 | ) 24 | ), 25 | create_card( 26 | class = "my-3 shadow-sm", 27 | contact_form() 28 | ) 29 | ) 30 | ) 31 | } 32 | 33 | #' Contact form 34 | #' 35 | #' @export 36 | contact_form <- \() { 37 | tags$form( 38 | `hx-target` = "this", 39 | `hx-post` = "/contact", 40 | `hx-swap` = "outerHTML", 41 | email_input(), 42 | tags$div( 43 | class = "row", 44 | tags$div( 45 | class = "col-12 col-md-6", 46 | name_input(kind = "First") 47 | ), 48 | tags$div( 49 | class = "col-12 col-md-6", 50 | name_input(kind = "Last") 51 | ) 52 | ), 53 | text_area_input(), 54 | submit_btn() 55 | ) 56 | } 57 | 58 | #' Contact email input 59 | #' 60 | #' @param value Value of the input 61 | #' @param ... tags to append to end of div 62 | #' @param input_class Additional bootstrap classes for the input. Mostly used 63 | #' for validation. 64 | #' @return An object of class `shiny.tag` 65 | #' @export 66 | email_input <- \(..., value = "", input_class = NULL) { 67 | tags$div( 68 | class = "mb-3", 69 | `hx-target` = "this", 70 | `hx-swap` = "outerHTML", 71 | tags$label( 72 | `for` = "email", 73 | class = "form-label", 74 | "Email address" 75 | ), 76 | tags$input( 77 | type = "email", 78 | name = "email", 79 | id = "email", 80 | class = paste("form-control", input_class), 81 | required = NA, 82 | value = value, 83 | `hx-post` = "/contact/email" 84 | ), 85 | ... 86 | ) 87 | } 88 | 89 | #' Name input 90 | #' 91 | #' @param kind String. Kind of name. Either "First", "Middle", or "Last". 92 | #' @param value String. Value of the input. 93 | #' @return An object of class `shiny.tag` 94 | #' @export 95 | name_input <- \(kind = "First", value = "") { 96 | id <- paste0(kind, "name") 97 | tags$div( 98 | class = "mb-3", 99 | tags$label( 100 | `for` = id, 101 | class = "form-label", 102 | paste(kind, "name") 103 | ), 104 | tags$input( 105 | type = "text", 106 | name = id, 107 | id = id, 108 | class = "form-control", 109 | value = value 110 | ) 111 | ) 112 | } 113 | 114 | #' Text area input 115 | #' 116 | #' @param ... tags to append to end of div 117 | #' @param id Input id 118 | #' @param label Input label 119 | #' @param value Input value 120 | #' @param input_class Additional bootstrap classes for the input. Mostly used 121 | #' for validation. 122 | #' @export 123 | text_area_input <- \( 124 | ..., 125 | id = "msg", 126 | label = "Your message", 127 | value = "", 128 | input_class = NULL 129 | ) { 130 | tags$div( 131 | class = "mb-3", 132 | `hx-target` = "this", 133 | `hx-swap` = "outerHTML", 134 | tags$label( 135 | `for` = id, 136 | class = "form-label", 137 | label 138 | ), 139 | tags$textarea( 140 | class = paste("form-control", input_class), 141 | id = id, 142 | name = id, 143 | rows = "3", 144 | required = NA, 145 | `hx-post` = "/contact/message", 146 | value 147 | ), 148 | ... 149 | ) 150 | } 151 | 152 | #' Submit button 153 | #' 154 | #' @param class Button class 155 | #' @return An object of class `shiny.tag` 156 | #' @export 157 | submit_btn <- \(class = "btn btn-primary px-4 rounded-1") { 158 | tags$button( 159 | type = "submit", 160 | class = class, 161 | "Submit" 162 | ) 163 | } 164 | --------------------------------------------------------------------------------