├── .formatter.exs
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── README.md
├── autocomplete_testbed
├── .formatter.exs
├── .gitignore
├── README.md
├── assets
│ ├── css
│ │ └── app.css
│ ├── js
│ │ └── app.js
│ ├── package-lock.json
│ ├── package.json
│ ├── tailwind.config.js
│ └── vendor
│ │ └── topbar.js
├── config
│ ├── config.exs
│ ├── dev.exs
│ ├── prod.exs
│ ├── runtime.exs
│ └── test.exs
├── lib
│ ├── autocomplete_testbed.ex
│ ├── autocomplete_testbed
│ │ └── application.ex
│ ├── autocomplete_testbed_web.ex
│ └── autocomplete_testbed_web
│ │ ├── components
│ │ ├── core_components.ex
│ │ ├── layouts.ex
│ │ └── layouts
│ │ │ ├── app.html.heex
│ │ │ └── root.html.heex
│ │ ├── controllers
│ │ ├── error_html.ex
│ │ ├── error_json.ex
│ │ ├── page_controller.ex
│ │ ├── page_html.ex
│ │ └── page_html
│ │ │ └── home.html.heex
│ │ ├── endpoint.ex
│ │ ├── gettext.ex
│ │ ├── live
│ │ ├── autocompleted_form.ex
│ │ └── autocompleted_form.html.heex
│ │ ├── router.ex
│ │ └── telemetry.ex
├── mix.exs
├── mix.lock
├── priv
│ ├── gettext
│ │ ├── en
│ │ │ └── LC_MESSAGES
│ │ │ │ └── errors.po
│ │ └── errors.pot
│ └── static
│ │ ├── favicon.ico
│ │ ├── images
│ │ └── logo.svg
│ │ └── robots.txt
└── test
│ ├── autocomplete_testbed_web
│ ├── controllers
│ │ ├── error_html_test.exs
│ │ ├── error_json_test.exs
│ │ └── page_controller_test.exs
│ └── features
│ │ └── autocomplete_test.exs
│ ├── support
│ └── conn_case.ex
│ └── test_helper.exs
├── lib
└── autocomplete_input.ex
├── mix.exs
├── mix.lock
└── test
├── autocomplete_input_test.exs
└── test_helper.exs
/.formatter.exs:
--------------------------------------------------------------------------------
1 | # Used by "mix format"
2 | [
3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4 | ]
5 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Trigger Integration Tests
2 |
3 | on: push
4 |
5 | jobs:
6 | test:
7 | name: Build and run tests
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Checkout project
11 | uses: actions/checkout@v3
12 | - name: Set up Elixir
13 | uses: erlef/setup-beam@v1
14 | with:
15 | otp-version: '26.2.1' # Define the OTP version [required]
16 | elixir-version: '1.17.2-otp-26' # Define the elixir version [required]
17 | - name: Checkout
18 | uses: actions/checkout@v2
19 | - name: Install dependencies
20 | run: mix deps.get
21 | - name: Install autocomplete_testbed dependencies
22 | run: mix deps.get
23 | working-directory: ./autocomplete_testbed
24 | - name: install autocomplete_testbed assets
25 | run: npm install
26 | working-directory: autocomplete_testbed/assets
27 | - name: build autocomplete_testbed javascript
28 | run: mix assets.build
29 | working-directory: ./autocomplete_testbed
30 | - name: Run elixir tests
31 | run: mix test
32 | working-directory: ./autocomplete_testbed
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where third-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 |
22 | # Ignore package tarball (built via "mix hex.build").
23 | autocomplete_input-*.tar
24 |
25 | # Temporary files, for example, from tests.
26 | /tmp/
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AutocompleteInput
2 |
3 | This is an autocomplete component for Phoenix LiveView. It uses a form associated custom element, which means it appears in your form params just like any other input element in your form. It is designed to be simple to integrate with live view and fully stylable via css.
4 |
5 | ## Installation
6 |
7 | The [hex package](https://hex.pm/packages/autocomplete_input) can be installed
8 | by adding `autocomplete_input` to your list of dependencies in `mix.exs`:
9 |
10 | ```elixir
11 | def deps do
12 | [
13 | {:autocomplete_input, "~> 0.1.0"}
14 | ]
15 | end
16 | ```
17 |
18 | To install the `autocomplete-input` element javascript:
19 | ```
20 | npm install --prefix assets phoenix-custom-event-hook @launchscout/autocomplete-input
21 | ```
22 |
23 | Add following to app.js
24 |
25 | ```js
26 | import '@launchscout/autocomplete-input'
27 | import PhoenixCustomEventHook from 'phoenix-custom-event-hook'
28 |
29 | let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
30 | let liveSocket = new LiveSocket("/live", Socket, {
31 | longPollFallbackMs: 2500,
32 | params: {_csrf_token: csrfToken},
33 | hooks: { PhoenixCustomEventHook }
34 | })
35 | ```
36 | ## Usage
37 |
38 | The `AutocompleteInput.autocomplete_input` is a standard functional component that accepts the following attributes:
39 |
40 | * `id` - (Required) The unique identifier for the input element
41 | * `name` - (Required) The form field name that will be used in params
42 | * `options` - (Required) A list of options in the same form as `options_for_select`, eg `{label, value}`
43 | * `value` - (Optional) The initially selected value
44 | * `display_value` - (Optional) Text shown when the element renders in the intial closed state. An edit icon will be displayed next to it
45 | * `min_length` - (Optional, default: 1) Minimum number of characters required before showing suggestions
46 |
47 | ## Events
48 |
49 | The component sends two custom events that you can handle in your LiveView:
50 |
51 | * `autocomplete-search` - Triggered when the user types in the input field. The event payload contains:
52 | * `name` - The name of the field
53 | * `query` - The current search text entered by the user
54 |
55 | * `autocomplete-commit` - Triggered when an option is selected. It is normally expected you would clear the list of options here. The event payload will contain the following:
56 | * `name` The name of the field
57 | * `value` The selected value
58 |
59 | * `autocomplete-open` - Triggered when an autocomplete is opened. The event payload will contain
60 | * `name` The name of the field
61 |
62 | * `autocomplete-close` - Triggered when an autocomplete is cloase. It is normally expected that you will clear the list of options here. The event payload will contain
63 | * `name` The name of the field
64 |
65 | ## Example
66 |
67 | The component can be used within a form as follows:
68 |
69 | ```html
70 |
27 | //
28 | plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])),
29 | plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])),
30 | plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])),
31 |
32 | // Embeds Heroicons (https://heroicons.com) into your app.css bundle
33 | // See your `CoreComponents.icon/1` for more information.
34 | //
35 | plugin(function({matchComponents, theme}) {
36 | let iconsDir = path.join(__dirname, "../deps/heroicons/optimized")
37 | let values = {}
38 | let icons = [
39 | ["", "/24/outline"],
40 | ["-solid", "/24/solid"],
41 | ["-mini", "/20/solid"],
42 | ["-micro", "/16/solid"]
43 | ]
44 | icons.forEach(([suffix, dir]) => {
45 | fs.readdirSync(path.join(iconsDir, dir)).forEach(file => {
46 | let name = path.basename(file, ".svg") + suffix
47 | values[name] = {name, fullPath: path.join(iconsDir, dir, file)}
48 | })
49 | })
50 | matchComponents({
51 | "hero": ({name, fullPath}) => {
52 | let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "")
53 | let size = theme("spacing.6")
54 | if (name.endsWith("-mini")) {
55 | size = theme("spacing.5")
56 | } else if (name.endsWith("-micro")) {
57 | size = theme("spacing.4")
58 | }
59 | return {
60 | [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
61 | "-webkit-mask": `var(--hero-${name})`,
62 | "mask": `var(--hero-${name})`,
63 | "mask-repeat": "no-repeat",
64 | "background-color": "currentColor",
65 | "vertical-align": "middle",
66 | "display": "inline-block",
67 | "width": size,
68 | "height": size
69 | }
70 | }
71 | }, {values})
72 | })
73 | ]
74 | }
75 |
--------------------------------------------------------------------------------
/autocomplete_testbed/assets/vendor/topbar.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license MIT
3 | * topbar 2.0.0, 2023-02-04
4 | * https://buunguyen.github.io/topbar
5 | * Copyright (c) 2021 Buu Nguyen
6 | */
7 | (function (window, document) {
8 | "use strict";
9 |
10 | // https://gist.github.com/paulirish/1579671
11 | (function () {
12 | var lastTime = 0;
13 | var vendors = ["ms", "moz", "webkit", "o"];
14 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
15 | window.requestAnimationFrame =
16 | window[vendors[x] + "RequestAnimationFrame"];
17 | window.cancelAnimationFrame =
18 | window[vendors[x] + "CancelAnimationFrame"] ||
19 | window[vendors[x] + "CancelRequestAnimationFrame"];
20 | }
21 | if (!window.requestAnimationFrame)
22 | window.requestAnimationFrame = function (callback, element) {
23 | var currTime = new Date().getTime();
24 | var timeToCall = Math.max(0, 16 - (currTime - lastTime));
25 | var id = window.setTimeout(function () {
26 | callback(currTime + timeToCall);
27 | }, timeToCall);
28 | lastTime = currTime + timeToCall;
29 | return id;
30 | };
31 | if (!window.cancelAnimationFrame)
32 | window.cancelAnimationFrame = function (id) {
33 | clearTimeout(id);
34 | };
35 | })();
36 |
37 | var canvas,
38 | currentProgress,
39 | showing,
40 | progressTimerId = null,
41 | fadeTimerId = null,
42 | delayTimerId = null,
43 | addEvent = function (elem, type, handler) {
44 | if (elem.addEventListener) elem.addEventListener(type, handler, false);
45 | else if (elem.attachEvent) elem.attachEvent("on" + type, handler);
46 | else elem["on" + type] = handler;
47 | },
48 | options = {
49 | autoRun: true,
50 | barThickness: 3,
51 | barColors: {
52 | 0: "rgba(26, 188, 156, .9)",
53 | ".25": "rgba(52, 152, 219, .9)",
54 | ".50": "rgba(241, 196, 15, .9)",
55 | ".75": "rgba(230, 126, 34, .9)",
56 | "1.0": "rgba(211, 84, 0, .9)",
57 | },
58 | shadowBlur: 10,
59 | shadowColor: "rgba(0, 0, 0, .6)",
60 | className: null,
61 | },
62 | repaint = function () {
63 | canvas.width = window.innerWidth;
64 | canvas.height = options.barThickness * 5; // need space for shadow
65 |
66 | var ctx = canvas.getContext("2d");
67 | ctx.shadowBlur = options.shadowBlur;
68 | ctx.shadowColor = options.shadowColor;
69 |
70 | var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
71 | for (var stop in options.barColors)
72 | lineGradient.addColorStop(stop, options.barColors[stop]);
73 | ctx.lineWidth = options.barThickness;
74 | ctx.beginPath();
75 | ctx.moveTo(0, options.barThickness / 2);
76 | ctx.lineTo(
77 | Math.ceil(currentProgress * canvas.width),
78 | options.barThickness / 2
79 | );
80 | ctx.strokeStyle = lineGradient;
81 | ctx.stroke();
82 | },
83 | createCanvas = function () {
84 | canvas = document.createElement("canvas");
85 | var style = canvas.style;
86 | style.position = "fixed";
87 | style.top = style.left = style.right = style.margin = style.padding = 0;
88 | style.zIndex = 100001;
89 | style.display = "none";
90 | if (options.className) canvas.classList.add(options.className);
91 | document.body.appendChild(canvas);
92 | addEvent(window, "resize", repaint);
93 | },
94 | topbar = {
95 | config: function (opts) {
96 | for (var key in opts)
97 | if (options.hasOwnProperty(key)) options[key] = opts[key];
98 | },
99 | show: function (delay) {
100 | if (showing) return;
101 | if (delay) {
102 | if (delayTimerId) return;
103 | delayTimerId = setTimeout(() => topbar.show(), delay);
104 | } else {
105 | showing = true;
106 | if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
107 | if (!canvas) createCanvas();
108 | canvas.style.opacity = 1;
109 | canvas.style.display = "block";
110 | topbar.progress(0);
111 | if (options.autoRun) {
112 | (function loop() {
113 | progressTimerId = window.requestAnimationFrame(loop);
114 | topbar.progress(
115 | "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
116 | );
117 | })();
118 | }
119 | }
120 | },
121 | progress: function (to) {
122 | if (typeof to === "undefined") return currentProgress;
123 | if (typeof to === "string") {
124 | to =
125 | (to.indexOf("+") >= 0 || to.indexOf("-") >= 0
126 | ? currentProgress
127 | : 0) + parseFloat(to);
128 | }
129 | currentProgress = to > 1 ? 1 : to;
130 | repaint();
131 | return currentProgress;
132 | },
133 | hide: function () {
134 | clearTimeout(delayTimerId);
135 | delayTimerId = null;
136 | if (!showing) return;
137 | showing = false;
138 | if (progressTimerId != null) {
139 | window.cancelAnimationFrame(progressTimerId);
140 | progressTimerId = null;
141 | }
142 | (function loop() {
143 | if (topbar.progress("+.1") >= 1) {
144 | canvas.style.opacity -= 0.05;
145 | if (canvas.style.opacity <= 0.05) {
146 | canvas.style.display = "none";
147 | fadeTimerId = null;
148 | return;
149 | }
150 | }
151 | fadeTimerId = window.requestAnimationFrame(loop);
152 | })();
153 | },
154 | };
155 |
156 | if (typeof module === "object" && typeof module.exports === "object") {
157 | module.exports = topbar;
158 | } else if (typeof define === "function" && define.amd) {
159 | define(function () {
160 | return topbar;
161 | });
162 | } else {
163 | this.topbar = topbar;
164 | }
165 | }.call(this, window, document));
166 |
--------------------------------------------------------------------------------
/autocomplete_testbed/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Config module.
3 | #
4 | # This configuration file is loaded before any dependency and
5 | # is restricted to this project.
6 |
7 | # General application configuration
8 | import Config
9 |
10 | config :autocomplete_testbed,
11 | generators: [timestamp_type: :utc_datetime]
12 |
13 | # Configures the endpoint
14 | config :autocomplete_testbed, AutocompleteTestbedWeb.Endpoint,
15 | url: [host: "localhost"],
16 | adapter: Bandit.PhoenixAdapter,
17 | render_errors: [
18 | formats: [html: AutocompleteTestbedWeb.ErrorHTML, json: AutocompleteTestbedWeb.ErrorJSON],
19 | layout: false
20 | ],
21 | pubsub_server: AutocompleteTestbed.PubSub,
22 | live_view: [signing_salt: "Dz/OG7r8"]
23 |
24 | # Configure esbuild (the version is required)
25 | config :esbuild,
26 | version: "0.17.11",
27 | autocomplete_testbed: [
28 | args:
29 | ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
30 | cd: Path.expand("../assets", __DIR__),
31 | env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
32 | ]
33 |
34 | # Configure tailwind (the version is required)
35 | config :tailwind,
36 | version: "3.4.3",
37 | autocomplete_testbed: [
38 | args: ~w(
39 | --config=tailwind.config.js
40 | --input=css/app.css
41 | --output=../priv/static/assets/app.css
42 | ),
43 | cd: Path.expand("../assets", __DIR__)
44 | ]
45 |
46 | # Configures Elixir's Logger
47 | config :logger, :console,
48 | format: "$time $metadata[$level] $message\n",
49 | metadata: [:request_id]
50 |
51 | # Use Jason for JSON parsing in Phoenix
52 | config :phoenix, :json_library, Jason
53 |
54 | # Import environment specific config. This must remain at the bottom
55 | # of this file so it overrides the configuration defined above.
56 | import_config "#{config_env()}.exs"
57 |
--------------------------------------------------------------------------------
/autocomplete_testbed/config/dev.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | # For development, we disable any cache and enable
4 | # debugging and code reloading.
5 | #
6 | # The watchers configuration can be used to run external
7 | # watchers to your application. For example, we can use it
8 | # to bundle .js and .css sources.
9 | config :autocomplete_testbed, AutocompleteTestbedWeb.Endpoint,
10 | # Binding to loopback ipv4 address prevents access from other machines.
11 | # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
12 | http: [ip: {127, 0, 0, 1}, port: 4000],
13 | check_origin: false,
14 | code_reloader: true,
15 | debug_errors: true,
16 | secret_key_base: "Ko4SVYq6UM/Plte+gnAUz6uQr0c4vat4wyRxJJjG+UV0L3SinzMsNbkKz2pgs6bI",
17 | watchers: [
18 | esbuild: {Esbuild, :install_and_run, [:autocomplete_testbed, ~w(--sourcemap=inline --watch)]},
19 | tailwind: {Tailwind, :install_and_run, [:autocomplete_testbed, ~w(--watch)]}
20 | ]
21 |
22 | # ## SSL Support
23 | #
24 | # In order to use HTTPS in development, a self-signed
25 | # certificate can be generated by running the following
26 | # Mix task:
27 | #
28 | # mix phx.gen.cert
29 | #
30 | # Run `mix help phx.gen.cert` for more information.
31 | #
32 | # The `http:` config above can be replaced with:
33 | #
34 | # https: [
35 | # port: 4001,
36 | # cipher_suite: :strong,
37 | # keyfile: "priv/cert/selfsigned_key.pem",
38 | # certfile: "priv/cert/selfsigned.pem"
39 | # ],
40 | #
41 | # If desired, both `http:` and `https:` keys can be
42 | # configured to run both http and https servers on
43 | # different ports.
44 |
45 | # Watch static and templates for browser reloading.
46 | config :autocomplete_testbed, AutocompleteTestbedWeb.Endpoint,
47 | live_reload: [
48 | patterns: [
49 | ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$",
50 | ~r"priv/gettext/.*(po)$",
51 | ~r"lib/autocomplete_testbed_web/(controllers|live|components)/.*(ex|heex)$"
52 | ]
53 | ]
54 |
55 | # Enable dev routes for dashboard and mailbox
56 | config :autocomplete_testbed, dev_routes: true
57 |
58 | # Do not include metadata nor timestamps in development logs
59 | config :logger, :console, format: "[$level] $message\n"
60 |
61 | # Set a higher stacktrace during development. Avoid configuring such
62 | # in production as building large stacktraces may be expensive.
63 | config :phoenix, :stacktrace_depth, 20
64 |
65 | # Initialize plugs at runtime for faster development compilation
66 | config :phoenix, :plug_init_mode, :runtime
67 |
68 | config :phoenix_live_view,
69 | # Include HEEx debug annotations as HTML comments in rendered markup
70 | debug_heex_annotations: true,
71 | # Enable helpful, but potentially expensive runtime checks
72 | enable_expensive_runtime_checks: true
73 |
--------------------------------------------------------------------------------
/autocomplete_testbed/config/prod.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | # Note we also include the path to a cache manifest
4 | # containing the digested version of static files. This
5 | # manifest is generated by the `mix assets.deploy` task,
6 | # which you should run after static files are built and
7 | # before starting your production server.
8 | config :autocomplete_testbed, AutocompleteTestbedWeb.Endpoint,
9 | cache_static_manifest: "priv/static/cache_manifest.json"
10 |
11 | # Do not print debug messages in production
12 | config :logger, level: :info
13 |
14 | # Runtime production configuration, including reading
15 | # of environment variables, is done on config/runtime.exs.
16 |
--------------------------------------------------------------------------------
/autocomplete_testbed/config/runtime.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | # config/runtime.exs is executed for all environments, including
4 | # during releases. It is executed after compilation and before the
5 | # system starts, so it is typically used to load production configuration
6 | # and secrets from environment variables or elsewhere. Do not define
7 | # any compile-time configuration in here, as it won't be applied.
8 | # The block below contains prod specific runtime configuration.
9 |
10 | # ## Using releases
11 | #
12 | # If you use `mix release`, you need to explicitly enable the server
13 | # by passing the PHX_SERVER=true when you start it:
14 | #
15 | # PHX_SERVER=true bin/autocomplete_testbed start
16 | #
17 | # Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
18 | # script that automatically sets the env var above.
19 | if System.get_env("PHX_SERVER") do
20 | config :autocomplete_testbed, AutocompleteTestbedWeb.Endpoint, server: true
21 | end
22 |
23 | if config_env() == :prod do
24 | # The secret key base is used to sign/encrypt cookies and other secrets.
25 | # A default value is used in config/dev.exs and config/test.exs but you
26 | # want to use a different value for prod and you most likely don't want
27 | # to check this value into version control, so we use an environment
28 | # variable instead.
29 | secret_key_base =
30 | System.get_env("SECRET_KEY_BASE") ||
31 | raise """
32 | environment variable SECRET_KEY_BASE is missing.
33 | You can generate one by calling: mix phx.gen.secret
34 | """
35 |
36 | host = System.get_env("PHX_HOST") || "example.com"
37 | port = String.to_integer(System.get_env("PORT") || "4000")
38 |
39 | config :autocomplete_testbed, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
40 |
41 | config :autocomplete_testbed, AutocompleteTestbedWeb.Endpoint,
42 | url: [host: host, port: 443, scheme: "https"],
43 | http: [
44 | # Enable IPv6 and bind on all interfaces.
45 | # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
46 | # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0
47 | # for details about using IPv6 vs IPv4 and loopback vs public addresses.
48 | ip: {0, 0, 0, 0, 0, 0, 0, 0},
49 | port: port
50 | ],
51 | secret_key_base: secret_key_base
52 |
53 | # ## SSL Support
54 | #
55 | # To get SSL working, you will need to add the `https` key
56 | # to your endpoint configuration:
57 | #
58 | # config :autocomplete_testbed, AutocompleteTestbedWeb.Endpoint,
59 | # https: [
60 | # ...,
61 | # port: 443,
62 | # cipher_suite: :strong,
63 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
64 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
65 | # ]
66 | #
67 | # The `cipher_suite` is set to `:strong` to support only the
68 | # latest and more secure SSL ciphers. This means old browsers
69 | # and clients may not be supported. You can set it to
70 | # `:compatible` for wider support.
71 | #
72 | # `:keyfile` and `:certfile` expect an absolute path to the key
73 | # and cert in disk or a relative path inside priv, for example
74 | # "priv/ssl/server.key". For all supported SSL configuration
75 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
76 | #
77 | # We also recommend setting `force_ssl` in your config/prod.exs,
78 | # ensuring no data is ever sent via http, always redirecting to https:
79 | #
80 | # config :autocomplete_testbed, AutocompleteTestbedWeb.Endpoint,
81 | # force_ssl: [hsts: true]
82 | #
83 | # Check `Plug.SSL` for all available options in `force_ssl`.
84 | end
85 |
--------------------------------------------------------------------------------
/autocomplete_testbed/config/test.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | # We don't run a server during test. If one is required,
4 | # you can enable the server option below.
5 | config :autocomplete_testbed, AutocompleteTestbedWeb.Endpoint,
6 | http: [ip: {127, 0, 0, 1}, port: 4002],
7 | secret_key_base: "M0aTg700+CNTDNaf8dfCu+jT35DO44NxOQ5HxO+IeBQNsVmDXgWzaUJfgNNPiIsP",
8 | server: true
9 |
10 | # Print only warnings and errors during test
11 | config :logger, level: :warning
12 |
13 | # Initialize plugs at runtime for faster test compilation
14 | config :phoenix, :plug_init_mode, :runtime
15 |
16 | # Enable helpful, but potentially expensive runtime checks
17 | config :phoenix_live_view,
18 | enable_expensive_runtime_checks: true
19 |
20 | config :wallaby,
21 | otp_app: :autocomplete_testbed,
22 | base_url: "http://localhost:4002"
23 | # chromedriver: [
24 | # headless: false
25 | # ]
26 |
--------------------------------------------------------------------------------
/autocomplete_testbed/lib/autocomplete_testbed.ex:
--------------------------------------------------------------------------------
1 | defmodule AutocompleteTestbed do
2 | @moduledoc """
3 | AutocompleteTestbed keeps the contexts that define your domain
4 | and business logic.
5 |
6 | Contexts are also responsible for managing your data, regardless
7 | if it comes from the database, an external API or others.
8 | """
9 | end
10 |
--------------------------------------------------------------------------------
/autocomplete_testbed/lib/autocomplete_testbed/application.ex:
--------------------------------------------------------------------------------
1 | defmodule AutocompleteTestbed.Application do
2 | # See https://hexdocs.pm/elixir/Application.html
3 | # for more information on OTP Applications
4 | @moduledoc false
5 |
6 | use Application
7 |
8 | @impl true
9 | def start(_type, _args) do
10 | children = [
11 | AutocompleteTestbedWeb.Telemetry,
12 | {DNSCluster, query: Application.get_env(:autocomplete_testbed, :dns_cluster_query) || :ignore},
13 | {Phoenix.PubSub, name: AutocompleteTestbed.PubSub},
14 | # Start a worker by calling: AutocompleteTestbed.Worker.start_link(arg)
15 | # {AutocompleteTestbed.Worker, arg},
16 | # Start to serve requests, typically the last entry
17 | AutocompleteTestbedWeb.Endpoint
18 | ]
19 |
20 | # See https://hexdocs.pm/elixir/Supervisor.html
21 | # for other strategies and supported options
22 | opts = [strategy: :one_for_one, name: AutocompleteTestbed.Supervisor]
23 | Supervisor.start_link(children, opts)
24 | end
25 |
26 | # Tell Phoenix to update the endpoint configuration
27 | # whenever the application is updated.
28 | @impl true
29 | def config_change(changed, _new, removed) do
30 | AutocompleteTestbedWeb.Endpoint.config_change(changed, removed)
31 | :ok
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/autocomplete_testbed/lib/autocomplete_testbed_web.ex:
--------------------------------------------------------------------------------
1 | defmodule AutocompleteTestbedWeb do
2 | @moduledoc """
3 | The entrypoint for defining your web interface, such
4 | as controllers, components, channels, and so on.
5 |
6 | This can be used in your application as:
7 |
8 | use AutocompleteTestbedWeb, :controller
9 | use AutocompleteTestbedWeb, :html
10 |
11 | The definitions below will be executed for every controller,
12 | component, etc, so keep them short and clean, focused
13 | on imports, uses and aliases.
14 |
15 | Do NOT define functions inside the quoted expressions
16 | below. Instead, define additional modules and import
17 | those modules here.
18 | """
19 |
20 | def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
21 |
22 | def router do
23 | quote do
24 | use Phoenix.Router, helpers: false
25 |
26 | # Import common connection and controller functions to use in pipelines
27 | import Plug.Conn
28 | import Phoenix.Controller
29 | import Phoenix.LiveView.Router
30 | end
31 | end
32 |
33 | def channel do
34 | quote do
35 | use Phoenix.Channel
36 | end
37 | end
38 |
39 | def controller do
40 | quote do
41 | use Phoenix.Controller,
42 | formats: [:html, :json],
43 | layouts: [html: AutocompleteTestbedWeb.Layouts]
44 |
45 | use Gettext, backend: AutocompleteTestbedWeb.Gettext
46 |
47 | import Plug.Conn
48 |
49 | unquote(verified_routes())
50 | end
51 | end
52 |
53 | def live_view do
54 | quote do
55 | use Phoenix.LiveView,
56 | layout: {AutocompleteTestbedWeb.Layouts, :app}
57 |
58 | unquote(html_helpers())
59 | end
60 | end
61 |
62 | def live_component do
63 | quote do
64 | use Phoenix.LiveComponent
65 |
66 | unquote(html_helpers())
67 | end
68 | end
69 |
70 | def html do
71 | quote do
72 | use Phoenix.Component
73 |
74 | # Import convenience functions from controllers
75 | import Phoenix.Controller,
76 | only: [get_csrf_token: 0, view_module: 1, view_template: 1]
77 |
78 | # Include general helpers for rendering HTML
79 | unquote(html_helpers())
80 | end
81 | end
82 |
83 | defp html_helpers do
84 | quote do
85 | # Translation
86 | use Gettext, backend: AutocompleteTestbedWeb.Gettext
87 |
88 | # HTML escaping functionality
89 | import Phoenix.HTML
90 | # Core UI components
91 | import AutocompleteTestbedWeb.CoreComponents
92 |
93 | # Shortcut for generating JS commands
94 | alias Phoenix.LiveView.JS
95 |
96 | # Routes generation with the ~p sigil
97 | unquote(verified_routes())
98 | end
99 | end
100 |
101 | def verified_routes do
102 | quote do
103 | use Phoenix.VerifiedRoutes,
104 | endpoint: AutocompleteTestbedWeb.Endpoint,
105 | router: AutocompleteTestbedWeb.Router,
106 | statics: AutocompleteTestbedWeb.static_paths()
107 | end
108 | end
109 |
110 | @doc """
111 | When used, dispatch to the appropriate controller/live_view/etc.
112 | """
113 | defmacro __using__(which) when is_atom(which) do
114 | apply(__MODULE__, which, [])
115 | end
116 | end
117 |
--------------------------------------------------------------------------------
/autocomplete_testbed/lib/autocomplete_testbed_web/components/core_components.ex:
--------------------------------------------------------------------------------
1 | defmodule AutocompleteTestbedWeb.CoreComponents do
2 | @moduledoc """
3 | Provides core UI components.
4 |
5 | At first glance, this module may seem daunting, but its goal is to provide
6 | core building blocks for your application, such as modals, tables, and
7 | forms. The components consist mostly of markup and are well-documented
8 | with doc strings and declarative assigns. You may customize and style
9 | them in any way you want, based on your application growth and needs.
10 |
11 | The default components use Tailwind CSS, a utility-first CSS framework.
12 | See the [Tailwind CSS documentation](https://tailwindcss.com) to learn
13 | how to customize them or feel free to swap in another framework altogether.
14 |
15 | Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
16 | """
17 | use Phoenix.Component
18 | use Gettext, backend: AutocompleteTestbedWeb.Gettext
19 |
20 | alias Phoenix.LiveView.JS
21 |
22 | @doc """
23 | Renders a modal.
24 |
25 | ## Examples
26 |
27 | <.modal id="confirm-modal">
28 | This is a modal.
29 |
30 |
31 | JS commands may be passed to the `:on_cancel` to configure
32 | the closing/cancel event, for example:
33 |
34 | <.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}>
35 | This is another modal.
36 |
37 |
38 | """
39 | attr :id, :string, required: true
40 | attr :show, :boolean, default: false
41 | attr :on_cancel, JS, default: %JS{}
42 | slot :inner_block, required: true
43 |
44 | def modal(assigns) do
45 | ~H"""
46 |
53 |
54 |
62 |
63 |
64 | <.focus_wrap
65 | id={"#{@id}-container"}
66 | phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
67 | phx-key="escape"
68 | phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
69 | class="shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-white p-14 shadow-lg ring-1 transition"
70 | >
71 |
72 |
80 |
81 |
82 | {render_slot(@inner_block)}
83 |
84 |
85 |
86 |
87 |
88 |
89 | """
90 | end
91 |
92 | @doc """
93 | Renders flash notices.
94 |
95 | ## Examples
96 |
97 | <.flash kind={:info} flash={@flash} />
98 | <.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!
99 | """
100 | attr :id, :string, doc: "the optional id of flash container"
101 | attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
102 | attr :title, :string, default: nil
103 | attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
104 | attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
105 |
106 | slot :inner_block, doc: "the optional inner block that renders the flash message"
107 |
108 | def flash(assigns) do
109 | assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)
110 |
111 | ~H"""
112 |
hide("##{@id}")}
116 | role="alert"
117 | class={[
118 | "fixed top-2 right-2 mr-2 w-80 sm:w-96 z-50 rounded-lg p-3 ring-1",
119 | @kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
120 | @kind == :error && "bg-rose-50 text-rose-900 shadow-md ring-rose-500 fill-rose-900"
121 | ]}
122 | {@rest}
123 | >
124 |
125 | <.icon :if={@kind == :info} name="hero-information-circle-mini" class="h-4 w-4" />
126 | <.icon :if={@kind == :error} name="hero-exclamation-circle-mini" class="h-4 w-4" />
127 | {@title}
128 |
129 |
{msg}
130 |
133 |
134 | """
135 | end
136 |
137 | @doc """
138 | Shows the flash group with standard titles and content.
139 |
140 | ## Examples
141 |
142 | <.flash_group flash={@flash} />
143 | """
144 | attr :flash, :map, required: true, doc: "the map of flash messages"
145 | attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
146 |
147 | def flash_group(assigns) do
148 | ~H"""
149 |
150 | <.flash kind={:info} title={gettext("Success!")} flash={@flash} />
151 | <.flash kind={:error} title={gettext("Error!")} flash={@flash} />
152 | <.flash
153 | id="client-error"
154 | kind={:error}
155 | title={gettext("We can't find the internet")}
156 | phx-disconnected={show(".phx-client-error #client-error")}
157 | phx-connected={hide("#client-error")}
158 | hidden
159 | >
160 | {gettext("Attempting to reconnect")}
161 | <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
162 |
163 |
164 | <.flash
165 | id="server-error"
166 | kind={:error}
167 | title={gettext("Something went wrong!")}
168 | phx-disconnected={show(".phx-server-error #server-error")}
169 | phx-connected={hide("#server-error")}
170 | hidden
171 | >
172 | {gettext("Hang in there while we get back on track")}
173 | <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
174 |
175 |
176 | """
177 | end
178 |
179 | @doc """
180 | Renders a simple form.
181 |
182 | ## Examples
183 |
184 | <.simple_form for={@form} phx-change="validate" phx-submit="save">
185 | <.input field={@form[:email]} label="Email"/>
186 | <.input field={@form[:username]} label="Username" />
187 | <:actions>
188 | <.button>Save
189 |
190 |
191 | """
192 | attr :for, :any, required: true, doc: "the data structure for the form"
193 | attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
194 |
195 | attr :rest, :global,
196 | include: ~w(autocomplete name rel action enctype method novalidate target multipart),
197 | doc: "the arbitrary HTML attributes to apply to the form tag"
198 |
199 | slot :inner_block, required: true
200 | slot :actions, doc: "the slot for form actions, such as a submit button"
201 |
202 | def simple_form(assigns) do
203 | ~H"""
204 | <.form :let={f} for={@for} as={@as} {@rest}>
205 |
206 | {render_slot(@inner_block, f)}
207 |
208 | {render_slot(action, f)}
209 |
210 |
211 |
212 | """
213 | end
214 |
215 | @doc """
216 | Renders a button.
217 |
218 | ## Examples
219 |
220 | <.button>Send!
221 | <.button phx-click="go" class="ml-2">Send!
222 | """
223 | attr :type, :string, default: nil
224 | attr :class, :string, default: nil
225 | attr :rest, :global, include: ~w(disabled form name value)
226 |
227 | slot :inner_block, required: true
228 |
229 | def button(assigns) do
230 | ~H"""
231 |
242 | """
243 | end
244 |
245 | @doc """
246 | Renders an input with label and error messages.
247 |
248 | A `Phoenix.HTML.FormField` may be passed as argument,
249 | which is used to retrieve the input name, id, and values.
250 | Otherwise all attributes may be passed explicitly.
251 |
252 | ## Types
253 |
254 | This function accepts all HTML input types, considering that:
255 |
256 | * You may also set `type="select"` to render a `