Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions lib/crane/browser/window/scripts/live_view/dom.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ defmodule LiveView.DOM do
}

def by_id(%Document{} = document, id) do
GenDOM.Doccument.get_element_by_id(document, id)
GenDOM.Document.get_element_by_id(document, id)
|> case do
nil ->
# log - "no id found for #{id}"
Expand Down Expand Up @@ -65,7 +65,7 @@ defmodule LiveView.DOM do
end

def find_upload_inputs(node) do
document = GenServer.call(node.owner_docuemnt, :get)
document = GenServer.call(node.owner_document, :get)
form_id = node.id
inputs_outside_form = all(document, ~s'input[type="file"][#{@phx_upload_ref}][form="#{form_id}"])')

Expand All @@ -76,7 +76,7 @@ defmodule LiveView.DOM do
all(document, ~s'[#{@phx_view_ref}="#{view_id}"][#{@phx_component}="#{cid}"]')
end

def is_phx_destoryed?(node) do
def is_phx_destroyed?(node) do
!!(node.id == "" && private(node, "destroyed"))
end

Expand Down Expand Up @@ -119,7 +119,7 @@ defmodule LiveView.DOM do
end

def find_existing_parent_cids(document, view_id, cids) do
{parent_cids, children_cids} =
{parent_cids, children_cids} =
Enum.reduce(cids, {MapSet.new(), MapSet.new()}, fn(cid, {parent_cids, children_cids}) ->
all(document, ~s'[#{@phx_view_ref}="#{view_id}"][#{@phx_component}="#{cid}"]')
|> Enum.reduce({parent_cids, children_cids}, fn(parent, {parent_cids, children_cids}) ->
Expand Down Expand Up @@ -233,9 +233,9 @@ defmodule LiveView.DOM do
else
callback.()
task = Task.async(fn ->
:timer.sleep timout
:timer.sleep timeout
if async_filter.(),
trigger_cycle(element, @debounce_trigger)
do: trigger_cycle(element, @debounce_trigger)
end)

put_private(element, @throttled, task)
Expand All @@ -252,5 +252,14 @@ defmodule LiveView.DOM do
end
end
end


def inc_cycle(a, b, c), do: raise "not implemented"

def once(a, b), do: raise "not implemented"

def is_phx_child?(element), do: raise "not implemented"

def trigger_cycle(element, trigger, cycle \\ nil), do: raise "not implemented"

def find_phx_children(a, b), do: raise "not implemented"
end
6 changes: 3 additions & 3 deletions lib/crane/browser/window/scripts/live_view/dom_patch.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ defmodule LiveView.DOMPatch do
view: nil,
live_socket: nil,
container: nil,
id: nil,
id: nil,
root_id: nil,
markup: nil,
streams: nil,
Expand Down Expand Up @@ -125,7 +125,7 @@ defmodule LiveView.DOMPatch do

def mark_prunable_content_for_removal(%__MODULE__{} = dom_patch) do
phx_update = LiveSocket.binding(dom_patch.live_socket, @phx_update)
container = Floki.find_and_udpate(dom_patch.container, "[#{phx_update}=appennd] > *, [#{phx_update}=prepend] > *", fn
container = Floki.find_and_update(dom_patch.container, "[#{phx_update}=append] > *, [#{phx_update}=prepend] > *", fn
{tag_name, attributes, children} ->
attributes =
Enum.into(attributes, %{})
Expand All @@ -145,6 +145,6 @@ defmodule LiveView.DOMPatch do
end

def set_stream_ref(el, ref) do

end
end
239 changes: 239 additions & 0 deletions lib/crane/browser/window/scripts/live_view/morphdom.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
defmodule LiveView.MorphDOM do
@element_node 1
@text_node 3

def morph(from_node, to_node, opts \\ []) do
morph(from_node, to_node, opts, %{})
end

defp morph(from_node, to_node, opts, keyed_nodes) do
get_node_key = Keyword.get(opts, :get_node_key, fn node ->
if node.id == "" do
nil
else
node.id
end
end)
skip_from_children = Keyword.get(opts, :skip_from_children, fn _node -> false end)
add_child = Keyword.get(opts, :add_child, &GenDOM.Node.append_child/2)
on_before_node_added = Keyword.get(opts, :on_before_node_added, fn node -> node end)
on_node_added = Keyword.get(opts, :on_node_added, fn node -> node end)
on_before_node_discarded = Keyword.get(opts, :on_before_node_discarded, fn _node -> true end)
on_node_discarded = Keyword.get(opts, :on_node_discarded, fn node -> node end)
on_before_element_updated = Keyword.get(opts, :on_before_element_updated, fn from_element, _to_element -> from_element end)
on_element_updated = Keyword.get(opts, :on_element_updated, fn element -> element end)
children_only = Keyword.get(opts, :children_only)

from_node = GenDOM.Node.get(from_node)
to_node = GenDOM.Node.get(to_node)

zipped_children = :lists.zip(
Enum.map(from_node.child_nodes, &GenDOM.Node.get/1),
Enum.map(to_node.child_nodes, &GenDOM.Node.get/1),
{:pad, {nil, nil}}
)
Enum.reduce(zipped_children, keyed_nodes, fn
{from_child, nil}, acc -> # element removed
on_before_node_discarded.(from_child)
GenDOM.Node.remove_child(from_node, from_child)
on_node_discarded.(from_child)
if key = get_node_key.(from_child) do
# keep track of keyed nodes, we might match them later
Map.put(acc, key, from_child)
else
acc
end
{nil, to_child}, acc -> # element added
on_before_node_added.(to_child)
add_child.(from_node, to_child)
on_node_added.(to_child)
acc
{%{ node_type: @element_node } = from_child, %{ node_type: @element_node } = to_child}, acc -> # element changed
if existing_child = Map.get(acc, get_node_key.(to_child)) do

end
on_before_element_updated.(from_child, to_child)
Enum.each([:tag_name, :attributes, :class_list], fn prop ->
new_value = Map.get(to_child, prop)
if Map.get(from_child, prop) != new_value do
GenDOM.Node.put(from_child, prop, new_value)
end
end)
on_element_updated.(from_child)
morph(
from_child,
to_child,
opts,
if key = get_node_key.(from_child) do
Map.delete(acc, key)
else
acc
end
)
{
%{ node_type: @text_node, whole_text: from_whole_text } = from_child,
%{ node_type: @text_node, whole_text: to_whole_text } = _to_child
}, acc when from_whole_text != to_whole_text -> # text changed
GenDOM.Node.put(from_child, :whole_text, to_whole_text)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try GenDOM.Node.put! I added that alternate form last week and it does a GenServer.cast instead of GenServer.call internally. This way you can update the state but avoid the blocking function call

acc
{%{ node_type: from_node_type } = from_child, %{ node_type: to_node_type } = to_child}, acc when from_node_type != to_node_type -> # text -> element or element -> text
on_before_node_discarded.(from_child)
on_before_node_added.(to_child)
# replace the child
GenDOM.Node.replace_child(from_node, to_child, from_child)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should probably add bang versions of the DOM functions as well. There are a ton of fire-and-forget DOM manipulation calls in client that we don't need to block on

on_node_discarded.(from_child)
on_node_added.(to_child)
if key = get_node_key.(from_child) do
Map.put(acc, key, from_child)
else
acc
end
end)
end

# def morphdom(from_node, to_node, opts \\ []) do

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commented out section is an implementation I had started on that was more of a clone of the JS, but I abandoned it to get something working in a more functional style.

# get_node_key = Keyword.get(opts, :get_node_key, fn _node -> nil end)
# skip_from_children = Keyword.get(opts, :skip_from_children, fn _node -> false end)
# add_child = Keyword.get(opts, :add_child, &GenDOM.Node.append_child/2)
# on_before_node_added = Keyword.get(opts, :on_before_node_added, fn node -> node end)
# on_node_added = Keyword.get(opts, :on_node_added, fn node -> node end)
# on_before_node_discarded = Keyword.get(opts, :on_before_node_discarded, fn _node -> true end)
# on_node_discarded = Keyword.get(opts, :on_node_discarded, fn node -> node end)
# on_before_element_updated = Keyword.get(opts, :on_before_element_updated, fn from_element, to_element -> from_element end)
# on_element_updated = Keyword.get(opts, :on_element_updated, fn element -> element end)
# children_only = Keyword.get(opts, :children_only)

# from_node = GenDOM.Node.get(from_node)
# morphed_node = from_node
# to_node = GenDOM.Node.get(to_node)

# if not Keyword.get(opts, :children_only) do
# # handle two dom nodes that are not compatible.
# # (e.g. <div> --> <span> or <div> --> TEXT)
# raise "todo"
# end

# morphed_node = if GenDOM.Node.is_same_node?(to_node, morphed_node) do
# Keyword.get(opts, :on_node_discarded).(from_node)
# morphed_node
# else
# {morphed_node, keyed_removal_list, from_nodes_lookup} = morph_element(morphed_node, to_node, [], %{}, opts)

# # We now need to loop over any keyed nodes that might need to be
# # removed. We only do the removal if we know that the keyed node
# # never found a match. When a keyed node is matched up we remove
# # it out of fromNodesLookup and we use fromNodesLookup to determine
# # if a keyed node has been matched up or not
# for key <- keyed_removal_list do
# element = Map.get(from_nodes_lookup, key)
# remove_node(element, element.parent, false, opts)
# end

# if children_only and not GenDOM.Node.is_same_node?(morphed_node, from_node) and from_node.parent_node do
# # If we had to swap out the from node with a new node because the old
# # node was not compatible with the target node then we need to
# # replace the old DOM node in the original DOM tree. This is only
# # possible if the original DOM node was part of a DOM tree which
# # we know is the case if it has a parent node.
# raise "todo"
# else
# morphed_node
# end
# end
# end

# # Removes a DOM node out of the original DOM.
# defp remove_node(node, parent, skip_keyed_nodes, opts) do
# if not Keyword.get(opts, :on_before_node_discarded).(node) do
# node
# else
# if parent != nil do
# GenDOM.Node.remove_child(parent, node)
# end

# Keyword.get(opts, :on_node_discarded).(node)
# walk_discarded_child_nodes(node, skip_keyed_nodes)
# end
# end

# # walks the children of the node and returns a list of keyed nodes.
# defp walk_discarded_child_nodes(node, skip_keyed_nodes, opts) when node.node_type == @element_node do
# node = GenDOM.Node.get(node)
# if node.first_child do
# walk_discarded_nodes(node.first_child, skip_keyed_nodes, opts)
# else
# []
# end
# end

# defp walk_discarded_nodes(node, skip_keyed_nodes, keyed_removal, opts) when node.node_type == @element_node do
# key = get_node_key(opts, node)
# keyed_removal = if skip_keyed_nodes and key do
# # If we are skipping keyed nodes then we add the key
# # to a list so that it can be handled at the very end.
# [key | keyed_removal]
# else
# # Only report the node as discarded if it is not keyed. We do this because
# # at the end we loop through all keyed elements that were unmatched
# # and then discard them in one final pass.
# opts.on_node_discarded(node)
# walk_discarded_child_nodes(node, skip_keyed_nodes, opts)
# end
# walk_discarded_nodes(GenDOM.Node.get(node).next_sibling, skip_keyed_nodes, opts)
# end

# defp walk_discarded_nodes(_node, _skip_keyed_nodes, keyed_removal, _opts) do
# keyed_removal
# end

# defp morph_element(from_node, to_node, keyed_removal_list, from_nodes_lookup, opts) do
# to_key = opts.get_node_key(to_node)
# from_nodes_lookup = if to_key do
# Map.delete(from_nodes_lookup, to_key)
# else
# from_nodes_lookup
# end

# if not opts.children_only do
# raise "todo"
# end

# morph_children(from_node, to_node, keyed_removal_list, from_nodes_lookup, opts)
# end

# defp morph_children(from_node, to_node, keyed_removal_list, from_nodes_lookup, opts) do
# skip_from = opts.skip_from_children(from_node, to_node)

# walk_to_children = fn to_node_child ->
# to_next_sibling = to_node_child.next_sibling
# to_node_key = opts.get_node_key(to_node_child)

# # walk the from_node children all the way through
# # looks for one that matches the to_node_child
# for from_child <- from_node.child_nodes do
# if GenDOM.Node.is_same_node?(to_node_child, from_child) do

# end
# end
# end

# walk_to_children(to_node.first_child)

# {morphed_node, keyed_removal_list, from_nodes_lookup}
# end

# defp compare_node_names(from_node, to_node) do
# raise "todo"
# end

# defp move_children(from_node, to_node) do
# raise "todo"
# to_node
# end

# defp get_node_key(opts, node) do
# Keyword.get(opts, :get_node_key, fn node ->
# GenDOM.Element.get_attribute(node, "id")
# end).(node)
# end
end
Loading