GithubHelp home page GithubHelp logo

phoenixframework / phoenix_live_view Goto Github PK

View Code? Open in Web Editor NEW
6.0K 6.0K 900.0 14.21 MB

Rich, real-time user experiences with server-rendered HTML

Home Page: https://hex.pm/packages/phoenix_live_view

License: MIT License

Elixir 63.71% JavaScript 36.24% HTML 0.05%

phoenix_live_view's Introduction

Phoenix LiveView

Actions Status Hex.pm Documentation

Phoenix LiveView enables rich, real-time user experiences with server-rendered HTML.

Visit the https://livebeats.fly.dev demo to see the kinds of applications you can build, or see a sneak peek below:

Phoenix.LiveView.LiveBeats.Demo.mp4

After you install Elixir on your machine, you can create your first LiveView app in two steps:

$ mix archive.install hex phx_new
$ mix phx.new demo

Feature highlights

LiveView brings a unified experience to building web applications. You no longer have to split work between client and server, across different toolings, layers, and abstractions. Instead, LiveView enriches the server with a declarative and powerful model while keeping your code closer to your data (and ultimately your source of truth):

  • Declarative rendering: Render HTML on the server over WebSockets with a declarative model, including an optional LongPolling fallback.

  • Rich templating language: Enjoy HEEx: a templating language that supports function components, slots, HTML validation, verified routes, and more.

  • Diffs over the wire: Instead of sending "HTML over the wire", LiveView knows exactly which parts of your templates change, sending minimal diffs over the wire after the initial render, reducing latency and bandwidth usage. The client leverages this information and optimizes the browser with 5-10x faster updates, compared to solutions that replace whole HTML fragments.

  • Live form validation: LiveView supports real-time form validation out of the box. Create rich user interfaces with features like uploads, nested inputs, and specialized recovery.

  • File uploads: Real-time file uploads with progress indicators and image previews. Process your uploads on the fly or submit them to your desired cloud service.

  • Rich integration API: Use the rich integration API to interact with the client, with phx-click, phx-focus, phx-blur, phx-submit, and phx-hook included for cases where you have to write JavaScript.

  • Optimistic updates and transitions: Perform optimistic updates and transitions with JavaScript commands via Phoenix.LiveView.JS.

  • Loose coupling: Reuse more code via stateful components with loosely-coupled templates, state, and event handling — a must for enterprise application development.

  • Live navigation: Enriched links and redirects are just more ways LiveView keeps your app light and performant. Clients load the minimum amount of content needed as users navigate around your app without any compromise in user experience.

  • Latency simulator: Emulate how slow clients will interact with your application with the latency simulator.

  • Robust test suite: Write tests with confidence alongside Phoenix LiveView built-in testing tools. No more running a whole browser alongside your tests.

Learning

Check our comprehensive docs to get started.

The Phoenix framework documentation also keeps a list of community resources, including books, videos, and other materials, and some include LiveView too.

Also follow these announcements from the Phoenix team on LiveView for more examples and rationale:

Installation

LiveView is included by default in all new Phoenix v1.6+ applications and later. If you have an older existing Phoenix app and you wish to add LiveView, see the previous installation guide.

What makes LiveView unique?

LiveView is server-centric. You no longer have to worry about managing both client and server to keep things in sync. LiveView automatically updates the client as changes happen on the server.

LiveView is first rendered statically as part of regular HTTP requests, which provides quick times for "First Meaningful Paint", in addition to helping search and indexing engines.

Then LiveView uses a persistent connection between client and server. This allows LiveView applications to react faster to user events as there is less work to be done and less data to be sent compared to stateless requests that have to authenticate, decode, load, and encode data on every request.

When LiveView was first announced, many developers from different backgrounds got inspired by the potential unlocked by LiveView to build rich, real-time user experiences. We believe LiveView is built on top of a solid foundation that makes LiveView hard to replicate anywhere else:

  • LiveView is built on top of the Elixir programming language and functional programming, which provides a great model for reasoning about your code and how your LiveView changes over time.

  • By building on top of a scalable platform, LiveView scales well vertically (from small to large instances) and horizontally (by adding more instances). This allows you to continue shipping features when more and more users join your application, instead of dealing with performance issues.

  • LiveView applications are distributed and real-time. A LiveView app can push events to users as those events happen anywhere in the system. Do you want to notify a user that their best friend just connected? This is easily done without a single line of custom JavaScript and with no extra external dependencies (no extra databases, no Redis, no extra message queues, etc.).

  • LiveView performs change tracking: whenever you change a value on the server, LiveView will send to the client only the values that changed, drastically reducing the latency and the amount of data sent over the wire. This is achievable thanks to Elixir's immutability and its ability to treat code as data.

Browser Support

All current Chrome, Safari, Firefox, and MS Edge are supported. IE11 support is available with the following polyfills:

$ npm install --save --prefix assets mdn-polyfills url-search-params-polyfill formdata-polyfill child-replace-with-polyfill classlist-polyfill new-event-polyfill @webcomponents/template shim-keyboard-event-key core-js

Note: The shim-keyboard-event-key polyfill is also required for MS Edge 12-18.

Note: The event-submitter-polyfill package is also required for MS Edge 12-80 & Safari < 15.4.

// assets/js/app.js
import "mdn-polyfills/Object.assign"
import "mdn-polyfills/CustomEvent"
import "mdn-polyfills/String.prototype.startsWith"
import "mdn-polyfills/Array.from"
import "mdn-polyfills/Array.prototype.find"
import "mdn-polyfills/Array.prototype.some"
import "mdn-polyfills/NodeList.prototype.forEach"
import "mdn-polyfills/Element.prototype.closest"
import "mdn-polyfills/Element.prototype.matches"
import "mdn-polyfills/Node.prototype.remove"
import "child-replace-with-polyfill"
import "url-search-params-polyfill"
import "formdata-polyfill"
import "classlist-polyfill"
import "new-event-polyfill"
import "@webcomponents/template"
import "shim-keyboard-event-key"
import "event-submitter-polyfill"
import "core-js/features/set"
import "core-js/features/url"

import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
...

Contributing

We appreciate any contribution to LiveView.

Please see the Phoenix Code of Conduct and Contributing guides.

Running the Elixir tests:

$ mix deps.get
$ mix test

Running all JavaScript tests:

$ npm run test

Running the JavaScript unit tests:

$ cd assets
$ npm install
$ npm run test
# to automatically run tests for files that have been changed
$ npm run test.watch

or simply:

$ npm run js:test

Running the JavaScript end-to-end tests:

$ npm run e2e:test

Checking test coverage:

$ npm run cover
$ npm run cover:report

JS contributions are very welcome, but please do not include an updated priv/static/phoenix_live_view.js in pull requests. The maintainers will update it as part of the release process.

phoenix_live_view's People

Contributors

aaronrenner avatar alexgaribay avatar aptinio avatar bcardarella avatar chrismccord avatar connorlay avatar dependabot[bot] avatar dvic avatar feliperenan avatar gazler avatar henrik avatar jonatanklosko avatar jonrowe avatar josevalim avatar joshprice avatar leandrocp avatar lostkobrakai avatar mcrumm avatar msaraiva avatar mveytsman avatar nathanl avatar preciz avatar raygesualdo avatar rhcarvalho avatar rktjmp avatar seb3s avatar sfusato avatar snewcomer avatar steffende avatar wojtekmach avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

phoenix_live_view's Issues

Provide life-cycle CSS classes

Today, we have life-cycle classes only at the LiveView root: phx-disconnected and phx-loading. We should document those accordingly.

I also suggest to provide life-cycles for every event. For example, if a button is clicked, a phx-clicked class will be added until click "response" is received from the server. Similar for phx-change and phx-submit events on a form. This probably requires #569 to be implemented.

phx-keyup overrides phx-keydown

phx-keyup appears to override phx-keydown when used together.

Environment

  • Elixir version (elixir -v):
Erlang/OTP 21 [erts-10.2.1] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]

Elixir 1.7.4 (compiled with Erlang/OTP 20)
  • Phoenix version (mix deps):
* phoenix 1.4.2 (Hex package) (mix)
  locked at 1.4.2 (phoenix) 3a1250f2
  ok
* phoenix_live_reload 1.2.0 (Hex package) (mix)
  locked at 1.2.0 (phoenix_live_reload) 3bb31a9f
  ok
* phoenix_html 2.13.1 (https://github.com/phoenixframework/phoenix_html.git) (mix)
  locked at fd5036c
  ok
* phoenix_live_view 0.1.0 (https://github.com/phoenixframework/phoenix_live_view.git) (mix)
  locked at 56482db
  ok
  • NodeJS version (node -v):
v8.15.0
  • NPM version (npm -v):
6.4.1
  • Operating system:
Linux MintyMac 4.15.0-46-generic #49-Ubuntu SMP Wed Feb 6 09:33:07 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

Repro code

defmodule DemoWeb.TestLive do
  use Phoenix.LiveView

  def mount(_session, socket) do
    {:ok, socket}
  end

  def render(assigns) do
    ~L"""
    <div phx-keydown="keydown" phx-keyup="keyup" phx-target="window">
    </div>
    """
  end

  def handle_event("keyup", key, socket) do
    IO.inspect key
    {:noreply, socket}
  end

  def handle_event("keydown", key, socket) do
    IO.inspect key
    {:noreply, socket}
  end

end

Actual behavior

When pressing a key I get the output of the key pressed only on the key up event. When removing the phx-keyup attribute, the keydown fires as expected.

Expected behavior

Should be getting two separate events for key up and key down, instead of only the key up event.

Notes

Might be related to #83

Layout in live routes

Environment

  • Phoenix version (mix deps): phoenix 1.4.2

Expected behavior

I have a live "/thing", ThingLive route in my router along with other controllers that have been piped through the :browser pipeline.

I expected the default :app LayoutView to apply to the ThingLive route like the rest of the controllers.

Actual behavior

No layout is applied and needs to be manually added to the pipeline like in the example app: https://github.com/chrismccord/phoenix_live_view_example/blob/13b07ae9a019fb89511d7f06f4634b992bc64627/lib/demo_web/router.ex#L11

Add latency simulator for dev

Browser dev tools today do not add latency for WS frames, so we'll need to simulate a tunable latency knob on the server for dev UX testing.

Multiple presses of the same key are ignored in phx-keypress and friends

Environment

  • Elixir version (elixir -v):
Erlang/OTP 21 [erts-10.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]

Elixir 1.8.1 (compiled with Erlang/OTP 20)
  • Phoenix version (mix deps):
* phoenix 1.4.2 (Hex package) (mix)
  locked at 1.4.2 (phoenix) 3a1250f2
  ok
* phoenix_live_reload 1.2.0 (Hex package) (mix)
  locked at 1.2.0 (phoenix_live_reload) 3bb31a9f
  ok
* phoenix_live_view 0.1.0 (https://github.com/phoenixframework/phoenix_live_view.git) (mix)
  locked at d77f787
  ok
  • NodeJS version (node -v):
v8.9.1
  • NPM version (npm -v):
6.9.0
  • Operating system:
Distributor ID: Ubuntu
Description:    Ubuntu 14.04.6 LTS
Release:        14.04
Codename:       trusty

Expected behavior

When pressing multiple keys in a row, only the first fires an event. This is due to the line below:

https://github.com/phoenixframework/phoenix_live_view/blob/master/assets/js/phoenix_live_view.js#L629

Actual behavior

It should register every keypress, even if a duplicate.

Handle "render_existing"

Environment

  • Phoenix version (mix deps): 1.4

Expected behavior

I'm using render_existing in <head> to conditionally render scripts and styles:
<%= render_existing(@view_module, "head." <> @view_template, assigns) %>

Should ignore if template doesn't exist.

Actual behavior

Raises no function clause matching in Phoenix.LiveView.Controller.render/2

Environment

  • Phoenix version (mix deps): 1.4

Expected behavior

I'm using render_existing in <head> to conditionally render scripts and styles:
<%= render_existing(@view_module, "head." <> @view_template, assigns) %>

Should ignore if template doesn't exist.

Actual behavior

Raises no function clause matching in Phoenix.LiveView.Controller.render/2

[info] GET /
[debug] Processing with LiveTestWeb.PageController.index/2
Parameters: %{}
Pipelines: [:browser]
[info] Sent 500 in 11ms
[error] #PID<0.5254.0> running LiveTestWeb.Endpoint (connection #PID<0.5246.0>, stream id 4) terminated
Server: localhost:4000 (http)
Request: GET /
** (exit) an exception was raised:
** (FunctionClauseError) no function clause matching in Phoenix.LiveView.Controller.render/2
(phoenix_live_view) lib/phoenix_live_view/controller.ex:72: Phoenix.LiveView.Controller.render("head.template.html", %{phx_render_existing: {Phoenix.LiveView.Controller, "head.template.html"}, conn: %Plug.Conn{adapter: {Plug.Cowboy.Conn, :...}, assigns: %{conn: %Plug.Conn{adapter: {Plug.Cowboy.Conn, :...}, assigns: %{current_member: nil, member_token: "SFMyNTY.g3QAAAACZAAEZGF0YW0AAAAiZ3Vlc3QtV2d5MVpOQWx0VWdmWXh2bnFFTElRdlo5ZExvPWQABnNpZ25lZG4GANaOQn5pAQ.eC3VCS6FLECsoWjYa851Njabig5CgtpNkZsZMbxT548"}, before_send: [#Function<0.76412652/1 in Plug.CSRFProtection.call/2>, #Function<2.70052748/1 in Phoenix.Controller.fetch_flash/2>, #Function<2.70052748/1 in Phoenix.Controller.fetch_flash/2>,#Function<0.58261320/1 in Plug.Session.before_send/2>, #Function<1.112466771/1 in Plug.Logger.call/2>, #Function<0.89070732/1 in Phoenix.LiveReloader.before_send_inject_reloader/2>], body_params: %{}, cookies: %{"_live_key" => "SFMyNTY.g3QAAAABbQAAAAtfY3NyZl90b2tlbm0AAAAYOHp1Vk9qeXEyeGdQSm9EQ0h1K1ljQT09.1_WO2tAMqyXADqSL58-4vz_0OaKYHfXqzPNL6X4YavA"}, halted: false, host: "localhost", method: "GET", owner: #PID<0.5254.0>, params: %{}, path_info: [], path_params: %{}, port: 4000, private: %{LiveTestWeb.Router => {[], %{}}, :phoenix_action => :index, :phoenix_controller => LiveTestWeb.PageController, :phoenix_endpoint => LiveTestWeb.Endpoint, :phoenix_flash => %{}, :phoenix_format => "html", :phoenix_layout=> {LiveTestWeb.LayoutView, :app}, :phoenix_pipelines => [:browser], :phoenix_router => LiveTestWeb.Router, :phoenix_view => LiveTestWeb.PageView, :plug_session => %{"_csrf_token" => "8zuVOjyq2xgPJoDCHu+YcA=="}, :plug_session_fetch => :done}, query_params: %{}, query_string: "", remote_ip: {127, 0, 0, 1}, req_cookies: %{"_live_key" => "SFMyNTY.g3QAAAABbQAAAAtfY3NyZl90b2tlbm0AAAAYOHp1Vk9qeXEyeGdQSm9EQ0h1K1ljQT09.1_WO2tAMqyXADqSL58-4vz_0OaKYHfXqzPNL6X4YavA"}, req_headers: [{"accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8"}, {"accept-encoding", "gzip, deflate, br"}, {"accept-language", "en-US,en;q=0.9,es;q=0.8"}, {"cache-control", "max-age=0"}, {"connection", "keep-alive"}, {"cookie", "_live_key=SFMyNTY.g3QAAAABbQAAAAtfY3NyZl90b2tlbm0AAAAYOHp1Vk9qeXEyeGdQSm9EQ0h1K1ljQT09.1_WO2tAMqyXADqSL58-4vz_0OaKYHfXqzPNL6X4YavA"}, {"host", "localhost:4000"}, {"referer", "http://localhost:4000/"}, {"upgrade-insecure-requests", "1"}, {"user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36"}], request_path: "/", resp_body: nil, resp_cookies: %{}, resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}, {"x-request-id", "2m5v5koo0rh6139r0s002b44"}, {"x-frame-options", "SAMEORIGIN"}, {"x-xss-protection", "1; mode=block"}, {"x-content-type-options", "nosniff"}, {"x-download-options", "noopen"}, {"x-permitted-cross-domain-policies", "none"}, {"cross-origin-window-policy", "deny"}], scheme: :http, script_name: [], secret_key_base: :..., state: :unset, status: nil}, content: {:safe, ["<div id="", "phx-h0+E+E9pIT8=", ""\n data-phx-view="", "LiveTestWeb.RoomLive", ""\n data-phx-session="", "SFMyNTY.g3QAAAACZAAEZGF0YW0AAACIZzNRQUFBQUVaQUFDYVdSdEFBQUFFSEJvZUMxb01DdEZLMFU1Y0VsVU9EMWtBQXB3WVhKbGJuUmZjR2xrWkFBRGJtbHNaQUFIYzJWemMybHZiblFBQUFBQVpBQUVkbWxsZDJRQUdFVnNhWGhwY2k1RVlXbHplVmRsWWk1U2IyOXRUR2wyWlE9PWQABnNpZ25lZG4GANaOQn5pAQ.wLi6SpYsIRMOAaD5qf00Tv334Ou-0RS5jZYi4vQMxK8", "">\n ", [" Current temperature:\n"], "\n\n<div class="phx-loader">\n"]}, current_member: nil, layout: {LiveTestWeb.LayoutView, "app.html"}, member_token: "SFMyNTY.g3QAAAACZAAEZGF0YW0AAAAiZ3Vlc3QtV2d5MVpOQWx0VWdmWXh2bnFFTElRdlo5ZExvPWQABnNpZ25lZG4GANaOQn5pAQ.eC3VCS6FLECsoWjYa851Njabig5CgtpNkZsZMbxT548"}, before_send: [#Function<0.76412652/1 in Plug.CSRFProtection.call/2>, #Function<2.70052748/1 in Phoenix.Controller.fetch_flash/2>, #Function<2.70052748/1 in Phoenix.Controller.fetch_flash/2>, #Function<0.58261320/1 in Plug.Session.before_send/2>, #Function<1.112466771/1 in Plug.Logger.call/2>, #Function<0.89070732/1 in Phoenix.LiveReloader.before_send_inject_reloader/2>], body_params: %{}, cookies: %{"_live_key" => "SFMyNTY.g3QAAAABbQAAAAtfY3NyZl90b2tlbm0AAAAYOHp1Vk9qeXEyeGdQSm9EQ0h1K1ljQT09.1_WO2tAMqyXADqSL58-4vz_0OaKYHfXqzPNL6X4YavA"}, halted: false, host: "localhost", method: "GET", owner: #PID<0.5254.0>, params: %{}, path_info: [], path_params: %{}, port: 4000, private: %{LiveTestWeb.Router => {[], %{}}, :phoenix_action => :index, :phoenix_controller => LiveTestWeb.PageController, :phoenix_endpoint => LiveTestWeb.Endpoint, :phoenix_flash => %{}, :phoenix_format => "html", :phoenix_layout => {LiveTestWeb.LayoutView, :app}, :phoenix_pipelines => [:browser], :phoenix_router => LiveTestWeb.Router, :phoenix_template => "template.html", :phoenix_view => Phoenix.LiveView.Controller, :plug_session => %{"_csrf_token" => "8zuVOjyq2xgPJoDCHu+YcA=="}, :plug_session_fetch => :done}, query_params: %{}, query_string: "", remote_ip: {127, 0, 0, 1}, req_cookies: %{"_live_key" => "SFMyNTY.g3QAAAABbQAAAAtfY3NyZl90b2tlbm0AAAAYOHp1Vk9qeXEyeGdQSm9EQ0h1K1ljQT09.1_WO2tAMqyXADqSL58-4vz_0OaKYHfXqzPNL6X4YavA"}, req_headers: [{"accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8"}, {"accept-encoding", "gzip, deflate, br"}, {"accept-language", "en-US,en;q=0.9,es;q=0.8"}, {"cache-control", "max-age=0"}, {"connection", "keep-alive"}, {"cookie", "_live_key=SFMyNTY.g3QAAAABbQAAAAtfY3NyZl90b2tlbm0AAAAYOHp1Vk9qeXEyeGdQSm9EQ0h1K1ljQT09.1_WO2tAMqyXADqSL58-4vz_0OaKYHfXqzPNL6X4YavA"}, {"host", "localhost:4000"}, {"referer", "http://localhost:4000/"}, {"upgrade-insecure-requests", "1"}, {"user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36"}], request_path: "/", resp_body: nil, resp_cookies: %{}, resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}, {"x-request-id", "2m5v5koo0rh6139r0s002b44"}, {"x-frame-options", "SAMEORIGIN"}, {"x-xss-protection", "1; mode=block"}, {"x-content-type-options", "nosniff"}, {"x-download-options", "noopen"}, {"x-permitted-cross-domain-policies", "none"}, {"cross-origin-window-policy", "deny"}], scheme: :http, script_name: [], secret_key_base: :..., state: :unset, status: nil}, content: {:safe, ["<div id="", "phx-h0+E+E9pIT8=", ""\n data-phx-view="", "LiveTestWeb.RoomLive", ""\n data-phx-session="", "SFMyNTY.g3QAAAACZAAEZGF0YW0AAACIZzNRQUFBQUVaQUFDYVdSdEFBQUFFSEJvZUMxb01DdEZLMFU1Y0VsVU9EMWtBQXB3WVhKbGJuUmZjR2xrWkFBRGJtbHNaQUFIYzJWemMybHZiblFBQUFBQVpBQUVkbWxsZDJRQUdFVnNhWGhwY2k1RVlXbHplVmRsWWk1U2IyOXRUR2wyWlE9PWQABnNpZ25lZG4GANaOQn5pAQ.wLi6SpYsIRMOAaD5qf00Tv334Ou-0RS5jZYi4vQMxK8", "">\n ", [" Current temperature:\n"], "\n\n<div class="phx-loader">\n"]}, current_member: nil,member_token: "SFMyNTY.g3QAAAACZAAEZGF0YW0AAAAiZ3Vlc3QtV2d5MVpOQWx0VWdmWXh2bnFFTElRdlo5ZExvPWQABnNpZ25lZG4GANaOQn5pAQ.eC3VCS6FLECsoWjYa851Njabig5CgtpNkZsZMbxT548", view_module: Phoenix.LiveView.Controller, view_template: "head.template.html"})
(live_test) lib/live_test_web/templates/layout/app.html.eex:8: LiveTestWeb.LayoutView."app.html"/1
(phoenix) lib/phoenix/view.ex:399: Phoenix.View.render_to_iodata/3
(phoenix) lib/phoenix/controller.ex:729: Phoenix.Controller.put_render/5
(phoenix) lib/phoenix/controller.ex:746: Phoenix.Controller.instrument_render_and_send/4
(live_test) lib/live_test_web/controllers/member_controller.ex:1: LiveTestWeb.PageController.action/2
(live_test) lib/live_test_web/controllers/member_controller.ex:1: LiveTestWeb.PageController.phoenix_controller_pipeline/2
(live_test) lib/live_test_web/endpoint.ex:1: LiveTestWeb.Endpoint.instrument/4
(phoenix) lib/phoenix/router.ex:275: Phoenix.Router.call/1
(live_test) (truncated)
[info] Replied phoenix:live_reload :ok

Document ~1 second WebSocket connect delay using localhost with Chrome browser

Environment

  • Elixir version (elixir -v): 1.8.1
  • Phoenix version (mix deps): 1.4.2
  • NodeJS version (node -v): v10.15.0
  • NPM version (npm -v): 6.5.0
  • Operating system: Windows 10

Expected behavior

There was no connection delay when using localhost in Chrome browser on Windows

Actual behavior

There is ~1 second delay is when using localhost, but 127.0.0.1 isn't effected. More info here https://bugs.chromium.org/p/chromium/issues/detail?id=175237
I think this should be documented.

Add phx-debounce

We should also remove the default throttling we have onkeypress.

Typespec for params is map, however with phx-keyup params is a binary

Environment

  • Elixir version (elixir -v): 1.8.1
  • Phoenix version (mix deps): 1.4.2
  • NodeJS version (node -v): -
  • NPM version (npm -v): -
  • Operating system: -

When using a phx-keyup binding, as in:

<input type="text" phx-keyup="new_query" />

... the handle_event/3 callback in a live view will receive its 2nd parameter, params as a binary. However, according to the typespec of the handle_event/3 callback in phoenix_live_view.ex (line 357), params should be a map:

@callback handle_event(event :: binary, unsigned_params, Socket.t()) :: {:noreply, Socket.t()} | {:stop, Socket.t()}
@type unsigned_params :: map

I will make a PR to change the type of unsigned_params in a moment, which would fix it. Let me know if the fix should be done in another way.

JavaScript bundle size - Separating live_view.js and phoenix.js

So I have some thoughts after digging around. Not necessary for action as there are probably other important things you have going on. But for discussion purposes....

At the moment, we have a live_view JS bundle dependent on 'phoenix.js'. Currently, webpack will build that dependency into our live_view JS bundle. We could assume a third party dependency like phoenix.js as a browser global, then use rollup to bundle live_view as an es module separately, which of course would still require the end user adding phoenix to their package.json.

That is to say in the current bundle for live_view, if you remove phoenix here in the package.json, everything still works .

I think there are other things going on b/c we are grabbing things from the file system ../deps, but I think it is possible with some thought put forth. This would strip out whatever the size that phoenix.js brings in.

Also, the reason I mention rollup is I would evaluate (and have heard) rollup vs webpack as rollup is best used for libraries and webpack is best used for applications but that is another discussion.

LiveView interfering with custom js

Expected behavior

Any custom javascript should execute either after LiveView loads, or maybe have an observable event similar to Turbolinks:

document.addEventListener("turbolinks:load", function() {
  // ...
})

Actual behavior

I'm seeing that LiveView often interferes with my custom js, but adding a delay fixes things. In my case using Stimulus:

import { Controller } from "stimulus";

export default class extends Controller {
  static targets = ["element"];

  connect() {
    const element = this.elementTarget;

    // THIS DOESN'T EXECUTE
      if (element.clientHeight < element.scrollHeight) {
        // Do something that modifies the DOM
      }

    // THIS DOES
    setTimeout(() => {
      if (element.clientHeight < element.scrollHeight) {
        // Do same thing
      }
    }, 200);
  }
}

Stand up initial js tests

I'm not up to speed on the latest in front-end testing, but we'll need DOM access for proper tests. My goal would be to keep it as simple as possible – which I'm not sure how much tinkering is involved for something like headless chrome vs jasmine in-browser tests. Would love your help and expertise here Scott!

phx-blur to be used instead of phx-change on forms

In terms of User eXperience, it makes more sense to trigger some validations when the user is leaving an input field, not on every key press or change.

For example to display if the user email is valid or not already taken, it would be better to do that when the user is leaving the email field only.

I'm open to comments on this feature but would be happy to provide a PR if I manage to do that...

Add phx-disable-with

Once we get a reply from the server, we always need to re-render, even if nothing changed on the server (which is the same thing we have to do for servers as outlined in #3).

Updating `phx-click` and `phx-value` attributes

Expected behavior

When LiveView updates an element's phx-click and phx-value attributes, the values are updated in the DOM, and click event triggers with new values.

Actual behavior

When LiveView updates an element's phx-click and phx-value attributes, the values are updated in the DOM, but the click event triggers as if the the original values were still there.

Example: clicking the first link works as expected - but the second triggers "show-something" rather than "show-something-else".

defmodule AppWeb.ExampleLive do
  use Phoenix.LiveView

  def render(assigns) do
    ~L"""
      <%= cond do %>
        <% @example-> %>
             <%= link to: "#", "phx-click": "show-something", "phx-value": @example.id do %>
                Link
              <% end %>
        <% @another -> %>
              <%= link to: "#", "phx-click": "show-something-else", "phx-value": @another.id do %>
                Link
              <% end %>
        <% true -> %>
          Lobby
      <% end %>
    """
  end

  def mount(%{example: example}, socket) do
    {:ok, assign(socket, example: example, another: nil)}
  end

  def handle_event("show-something", id, socket) do
    example = Example.get_example(id)
    {:noreply, assign(socket, example: example, another: nil)}
  end

  def handle_event("show-something-else", id, socket) do
    another = Another.get_another(id)
    {:noreply, assign(socket, example: nil, another: another)}
  end
end

Exception for missing signing salt suggests wrong fix

Environment

  • Elixir version (elixir -v): 1.8.1
  • Phoenix version (mix deps): 1.4.2
  • NodeJS version (node -v): 11.11.0
  • NPM version (npm -v): 6.7.0
  • Operating system: MacOS 10.14.3

Actual behavior

If signing salt is missing, the error message recommends the following fix:

Add the following LiveView configuration to your config/config.exs:

    config :my_app, MyApp.Endpoint,
        ...,
        live_view: [signing_salt: "EOFLaW48"]

Expected behavior

When the correct fix is:

Add the following LiveView configuration to your config/config.exs:

    config :my_app, MyAppWeb.Endpoint,
        ...,
        live_view: [signing_salt: "EOFLaW48"]

JavaScript-ty tasks

What can I do to help? Working on morphdom, understanding it and evaluating how other libraries handle DOM diffing. Are there other tasks you might need help on that are on your to do list?

Inserting non-live on live view breaks

Environment

  • Elixir version (elixir -v): Elixir 1.6.5 (compiled with OTP 20)
  • Phoenix version (mix deps): phoenix 1.4.0
  • NodeJS version (node -v): v8.15.1
  • NPM version (npm -v): 6.4.1
  • Operating system: macOS + Heroku

Expected behavior

I changed part of my template to use live view. I expect it to not change anything else on the template.

Actual behavior

The live view part actually works, but it breaks a non-live part of my template.
I have a loop that goes over DB elements with randomised order and render html for each (this line).

With the template as a live view, the elements load twice (you can see them reload twice on page refresh) and the UI breaks. It's a "flex" dive with expected 4 elements per row. After the live view "reload", there is always a few rows that end up having 3 elements only, and some divs seem to suffer from race conditions (it renders svg and some divs have svg parts from another div).

You can see the working behavior here: https://joeschmoe.io/ (the live-view diff was reverted). The loop mentioned is at the bottom of the page "all the schmoes".

The diff to insert live view is here: https://github.com/jonandjess/joeschmoe.io/pull/1/files.

What I've tried

Right now the data from the db is pulled directly from the template (using the all_originals() view helper).
I've tried to change that and load the data from the LiveView directly and pass it into the session object, but the same issue happens.

Not sure if I'm doing something wrong of this is an issue.

Screenshot

Here you can see a row has only 3 elements instead of 4, and "Jana" has 2 divs layered on top of each others (the teal behind is another character).
Screenshot 2019-03-18 at 09 02 29

Cheers!

input[type=password] value resetting

Environment

  • Elixir version (elixir -v):
    Elixir 1.7.3 (compiled with Erlang/OTP 21)

  • Phoenix version (mix deps):
    1.4.2

  • NodeJS version (node -v):
    v10.8.0

  • NPM version (npm -v):
    6.2.0

  • Operating system:
    macOS Mojave 10.14.3

Expected behavior

For all form inputs to retain state when monitored with phx_change event.

Actual behavior

The value of input[type=password] fields are reset when other fields are edited and trigger a phx_change event. This can be prevented by adding value: @changeset.changes[:password] to a password_input f, :password input field, forcing the value to be rendered in HTML resulting in a successful diff.

phx-change/phx-keypress has weird behaviour on input type="search"

Hello,

tested on Chrome 72 (OS X), take the DemoWeb.SearchLive example, replace in the render function:

<input type="text" ... by <input type="search" ...

The first key pressed in the input is not correctly registered and showing. The next keys pressed are normally registered.

Not sure if it's a bug of the JS library or not.

Routes.live_path does not prepend the path for forwarded routers

Environment

  • Elixir version (elixir -v): 1.8.1
  • Phoenix version (mix deps): 1.4.2
  • NodeJS version (node -v):
  • NPM version (npm -v):
  • Operating system:

Given some routers:

forward "/admin", AdminWeb.Router

In an AdminWeb.Router:

resources "/users", UserLive.Index

Actual behavior

Routes.live_path(@socket, UserLive.Index) #=> "/users"

Expected behavior

Routes.live_path(@socket, UserLive.Index) #=> "/admin/users"

Injected LiveView parent element breaks Flexbox layout

Expected behavior

Ability to modify / edit attributes (class, style, data-*, etc.) of the element injected by LiveView

Actual behavior

Injected LiveView element breaks layout when it's the child of a flexbox element

Should textarea changes respond to phx-change

The following does not send any change events when new text is entered. Is there a way to capture content changes? I can capture keys by adding a phx-keyup to the <textarea/> element, but this does not give me the contents of the textarea.

~L"""
    <form phx-change="change" phx-submit="submit">
      <textarea id="textarea" name="ta" value="<%= @input %>"></textarea>
    </form>
    """

Remove mount redirect

This means the option for now to fail a mount is to raise. When disconnected, the raising will be handled by the plug pipeline. When connected, it should be handled by live view which will redirect to the current page.

On the other hand, if we want to keep the redirect API, then we need to test it from a connected mount. Currently we only test redirect on disconnect.

Revisit session encoding

It appears we might be double encoding since Phoenix.Token should already be going the encode/decode work

Q: Documentation - Anti Patterns.

I think this maybe a tool that has huge potential to become quickly abused.

As someone who is new to sockets and the patterns starting to emerge from this tool I think it would be great to share opinions for new comers. I realize this maybe a big ask and will inherently take time so I completely understand if this ask never happens, but I thought its still work asking. Thanks again

phx-change event does not fire for forms in tables

Environment

  • Elixir version (elixir -v): 1.7.4 (Also duplicated with 1.8.1)
  • Phoenix version (mix deps): 1.4.2
  • NodeJS version (node -v): 11.3.0
  • NPM version (npm -v): 6.9.0
  • Operating system: macOSMojave 10.14.3
  • Browser: Chrome 72.0.3626.119

Expected behavior

When using a form that is embedded in a table like so:

    def render(assigns) do
      ~L"""
      <h2>Listing Things</h2>

      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Number</th>
            <th></th>
          </tr>
        </thead>
        <tbody>
      <%= for thing <- @things do %>
          <tr>
            <td><%= thing.name %></td>
            <td><%= thing.number %></td>
            <td>
              <button phx-click="delete_thing" phx-value="<%= thing.id %>">Delete</button>
            </td>
          </tr>
      <% end %>
          <tr>
            <%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save_new_thing], fn f -> %>
              <td>
                <%= text_input f, :name %>
                <%= error_tag f, :name %>
              </td>
              <td>
                <%= text_input f, :number %>
                <%= error_tag f, :number %>
              </td>
              <td><%= submit "Save", phx_disable_with: "Saving..." %></td>
            <% end %>
          </tr>
        </tbody>
      </table>
      <button phx-click="new_empty_thing">New Thing</button>
      """
    end

I'd expect the "validate" event to fire such that it can be handled via

    def handle_event("validate", %{"thing" => params}, socket) do
      changeset =
        %Thing{}
        |> Thing.changeset(params)
        |> Map.put(:action, :insert)

      {:noreply, assign(socket, changeset: changeset)}
    end

or be hit by my handler for unhandled events:

    def handle_event(unhandled_event, payload, socket) do
      IO.inspect unhandled_event, label: "unhandled_event"
      {:noreply, socket}
    end

Actual behavior

No event is received by LiveView handle_event/3 function.

Other things of note:

I tried changing the render/1 function such that the form was not inside the table like so:

    def render(assigns) do
      ~L"""
      <h2>Listing Things</h2>

      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Number</th>
            <th></th>
          </tr>
        </thead>
        <tbody>
      <%= for thing <- @things do %>
          <tr>
            <td><%= thing.name %></td>
            <td><%= thing.number %></td>
            <td>
              <button phx-click="delete_thing" phx-value="<%= thing.id %>">Delete</button>
            </td>
          </tr>
      <% end %>
          <tr>
          </tr>
        </tbody>
      </table>
      <button phx-click="new_empty_thing">New Thing</button>
      <%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save_new_thing], fn f -> %>

        <%= label f, :name %>
        <%= text_input f, :name %>
        <%= error_tag f, :name %>

        <%= label f, :number %>
        <%= text_input f, :number %>
        <%= error_tag f, :number %>

        <%= submit "Save", phx_disable_with: "Saving..." %>
      <% end %>
      """
    end

This results in the validation event properly firing and being handled as I'd expect.

The phx-submit event does properly fire when the form is inside of the table, resulting in a Thing record being created.

When the phx-submit event fires but there is a validation error, the changeset properly updates and the error validations appear when the form is in the table.

The handler for save looks like so:

    def handle_event("save_new_thing", %{"thing" => params}, socket) do
      case Validation.create_thing(params) do
        {:ok, thing} ->
          notify_subscribers(:save)
          all_things = fetch_all_things()
          changeset = Validation.change_thing()
          {:noreply, assign(socket, things: all_things, changeset: changeset)}

        {:error, %Ecto.Changeset{} = changeset} ->
          {:noreply, assign(socket, changeset: changeset)}
      end
    end

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.