diff --git a/assets/css/app.css b/assets/css/app.css index 093dee6..84b22ec 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -75,6 +75,10 @@ pre{ animation: slide-up 0.4s ease; } +.user-artwork { + border-radius: 50%; +} + @keyframes slide-up { 0% { opacity: 0; diff --git a/assets/js/app.js b/assets/js/app.js index 0e461a3..6396103 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -1,6 +1,7 @@ import css from "../css/app.css" import "phoenix_html" import {Socket} from "phoenix" +import IntersectionObserverAdmin from 'intersection-observer-admin'; import {LiveSocket, debug} from "phoenix_live_view" let Hooks = {} @@ -27,7 +28,112 @@ Hooks.InfiniteScroll = { updated(){ this.pending = this.page() } } -let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") +// TODO: make IE11 compat with rAF +const observerAdmin = new IntersectionObserverAdmin(); +const sentinelOptions = { rootMargin: '0px 0px 90px 0px', threshold: 0 }; +const observerOptions = { rootMargin: '0px 0px 0px 0px', threshold: 0 }; + +Hooks.ObserverInfiniteScroll = { + observerAdmin, + page() { return this.el.dataset.page }, + mounted(){ + this.pending = this.page() + let enterCallback = ({ target }) => { + if (this.pending == this.page()) { + this.pending = this.page() + 1 + this.pushEvent("load-more", {}) + } + } + + let exitCallback = ({ isIntersecting, target }) => { + if (isIntersecting) { + this.observerAdmin.unobserve(target, sentinelOptions); + } + } + + this.observerAdmin.addEnterCallback( + this.el, + enterCallback.bind(this) + ) + this.observerAdmin.addExitCallback( + this.el, + exitCallback.bind(this) + ) + + this.observerAdmin.observe( + this.el, + sentinelOptions + ) + }, + + // after DOM Patch + updated(){ this.pending = this.page() } +} + +Hooks.LazyArtwork = { + observerAdmin, + artwork() { return this.el.querySelector('img') }, + + mounted() { + window.requestIdleCallback(() => { + let enterCallback = ({ target: img }) => { + if (img) { + if (img && img.dataset) { + img.src = img.dataset.src; + } + } + } + + let exitCallback = ({ isIntersecting, target: img }) => { + if (isIntersecting) { + this.observerAdmin.unobserve(img, observerOptions); + } + } + + const artwork = this.artwork(); + this.observerAdmin.addEnterCallback( + artwork, + enterCallback + ) + this.observerAdmin.addExitCallback( + artwork, + exitCallback + ) + + this.observerAdmin.observe( + artwork, + observerOptions + ) + }); + } +} + +let serializeForm = (form) => { + let formData = new FormData(form) + let params = new URLSearchParams() + for(let [key, val] of formData.entries()){ params.append(key, val) } + + return params.toString() +} + +let Params = { + data: {}, + set(namespace, key, val){ + if(!this.data[namespace]){ this.data[namespace] = {}} + this.data[namespace][key] = val + }, + get(namespace){ return this.data[namespace] || {} } +} + +Hooks.SavedForm = { + mounted(){ + this.el.addEventListener("input", e => { + Params.set(this.viewName, "stashed_form", serializeForm(this.el)) + }) + } +} + +let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content"); let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, params: {_csrf_token: csrfToken}}) liveSocket.connect() diff --git a/assets/package-lock.json b/assets/package-lock.json index 2e6c06f..b32d7ba 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -4086,7 +4086,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -4107,12 +4108,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4127,17 +4130,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -4254,7 +4260,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -4266,6 +4273,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4280,6 +4288,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4287,12 +4296,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -4311,6 +4322,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -4391,7 +4403,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -4403,6 +4416,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -4488,7 +4502,8 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -4524,6 +4539,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4543,6 +4559,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4586,12 +4603,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -5265,6 +5284,12 @@ "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", "dev": true }, + "intersection-observer-admin": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/intersection-observer-admin/-/intersection-observer-admin-0.2.10.tgz", + "integrity": "sha512-qQo6SDLSSJYMDYo46b9rDA47VFiCn4pOhEMb97I4+iuFJBRfB28+nWpFtlAfkMwUyST30YNlovtebQOwsGKX1g==", + "dev": true + }, "into-stream": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", diff --git a/assets/package.json b/assets/package.json index 3ae1553..56e117f 100644 --- a/assets/package.json +++ b/assets/package.json @@ -22,6 +22,7 @@ "copy-webpack-plugin": "^4.5.0", "css-loader": "^0.28.10", "extract-text-webpack-plugin": "^4.0.0-beta.0", + "intersection-observer-admin": "^0.2.10", "morphdom": "^2.5.12", "style-loader": "^0.20.2", "webpack": "4.0.0", diff --git a/assets/static/images/1x1.gif b/assets/static/images/1x1.gif new file mode 100644 index 0000000..9d76ec6 Binary files /dev/null and b/assets/static/images/1x1.gif differ diff --git a/lib/demo/accounts/user.ex b/lib/demo/accounts/user.ex index 1c8c941..bc56070 100644 --- a/lib/demo/accounts/user.ex +++ b/lib/demo/accounts/user.ex @@ -3,6 +3,7 @@ defmodule Demo.Accounts.User do import Ecto.Changeset schema "users" do + field :artwork, :map field :username, :string field :email, :string field :phone_number, :string @@ -17,7 +18,7 @@ defmodule Demo.Accounts.User do @doc false def changeset(user, attrs) do user - |> cast(attrs, [:username, :email, :phone_number, :password]) + |> cast(attrs, [:username, :artwork, :email, :phone_number, :password]) |> validate_required([:username, :email, :phone_number]) |> validate_confirmation(:password) |> validate_format(:username, ~r/^[a-zA-Z0-9_]*$/, diff --git a/lib/demo_web/live/user_live/index_auto_scroll.ex b/lib/demo_web/live/user_live/index_auto_scroll.ex index 88dbe57..b580cf2 100644 --- a/lib/demo_web/live/user_live/index_auto_scroll.ex +++ b/lib/demo_web/live/user_live/index_auto_scroll.ex @@ -10,7 +10,7 @@ defmodule DemoWeb.UserLive.Row do def render(assigns) do ~L""" - + Email: <%= @email %> <%= @count %> """ @@ -28,6 +28,20 @@ defmodule DemoWeb.UserLive.Row do def render(assigns) do ~L""" + + ; + data-src=<%= Map.get(@user.artwork, "url") %> + alt=<%= @user.username %> + height=<%= Map.get(@user.artwork, "height") %> + width=<%= Map.get(@user.artwork, "width") %> + style="background-color: lightgray" + role="presentation" + phx-update="ignore" + data-lazy-artwork + /> + <%= @user.username %> <%= @count %> <%= live_component @socket, Email, id: "email-#{@id}", email: @user.email %> @@ -49,22 +63,21 @@ defmodule DemoWeb.UserLive.IndexAutoScroll do def render(assigns) do ~L""" - + <%= for user <- @users do %> <%= live_component @socket, Row, id: "user-#{user.id}", user: user %> <% end %>
+ +
""" end def mount(_session, socket) do {:ok, socket - |> assign(page: 1, per_page: 10) + |> assign(page: 1, per_page: 20) |> fetch(), temporary_assigns: [users: []]} end diff --git a/lib/demo_web/live/user_live/index_manual_scroll.ex b/lib/demo_web/live/user_live/index_manual_scroll.ex index 2b353a6..280f1b5 100644 --- a/lib/demo_web/live/user_live/index_manual_scroll.ex +++ b/lib/demo_web/live/user_live/index_manual_scroll.ex @@ -6,7 +6,20 @@ defmodule DemoWeb.UserLive.IndexManualScroll do <%= for user <- @users do %> - + + @@ -22,7 +35,7 @@ defmodule DemoWeb.UserLive.IndexManualScroll do def mount(_session, socket) do {:ok, socket - |> assign(page: 1, per_page: 10, val: 0) + |> assign(page: 1, per_page: 20, val: 0) |> fetch(), temporary_assigns: [users: []]} end diff --git a/priv/repo/migrations/20190918041148_add_artwork_to_user.exs b/priv/repo/migrations/20190918041148_add_artwork_to_user.exs new file mode 100644 index 0000000..c468a82 --- /dev/null +++ b/priv/repo/migrations/20190918041148_add_artwork_to_user.exs @@ -0,0 +1,9 @@ +defmodule Demo.Repo.Migrations.AddArtworkToUser do + use Ecto.Migration + + def change do + alter table(:users) do + add(:artwork, :map) + end + end +end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 22d419d..802bfae 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -10,11 +10,25 @@ # We recommend using the bang functions (`insert!`, `update!` # and so on) as they will fail if something goes wrong. + for i <- 1..1000 do + artworks = [ + "https://s3.amazonaws.com/uifaces/faces/twitter/jarjan/128.jpg", + "https://s3.amazonaws.com/uifaces/faces/twitter/aio___/128.jpg", + "https://s3.amazonaws.com/uifaces/faces/twitter/kolage/128.jpg", + "https://s3.amazonaws.com/uifaces/faces/twitter/sauro/128.jpg", + "https://s3.amazonaws.com/uifaces/faces/twitter/jina/128.jpg" + ] + {:ok, _} = Demo.Accounts.create_user(%{ username: "user#{i}", name: "User #{i}", + artwork: %{ + url: Enum.take_random(artworks, 1) |> Enum.at(0), + height: 65, + width: 65 + }, email: "user#{i}@test", phone_number: "555-555-5555" })
+ ; + data-src=<%= Map.get(user.artwork, "url") %> + alt=<%= user.username %> + height=<%= Map.get(user.artwork, "height") %> + width=<%= Map.get(user.artwork, "width") %> + style="background-color: lightgray" + role="presentation" + data-lazy-artwork + /> + <%= user.username %> <%= user.email %>