@@ -297,7 +297,8 @@ defmodule Phoenix.LiveView.Engine do
297297 mind the collection itself is not "diffed" across renders.
298298 If one entry in the comprehension changes, the whole collection
299299 is sent again. Consider using `Phoenix.LiveComponent` and
300- `Phoenix.LiveView.stream/4` to optimize those cases.
300+ `Phoenix.LiveView.stream/4` to optimize those cases, or see the
301+ [`:key` attribute when using :for`](Phoenix.Component.html#sigil_H/2-special-attributes).
301302
302303 The list of dynamics is always a list of iodatas or components,
303304 as we don't perform change tracking inside the comprehensions
@@ -422,6 +423,8 @@ defmodule Phoenix.LiveView.Engine do
422423 end
423424 end
424425
426+ root = Keyword . get ( opts , :root , meta [ :root ] )
427+
425428 { :ok ,
426429 quote do
427430 dynamic = fn track_changes? ->
@@ -434,7 +437,7 @@ defmodule Phoenix.LiveView.Engine do
434437 static: unquote ( static ) ,
435438 dynamic: dynamic ,
436439 fingerprint: unquote ( fingerprint ) ,
437- root: unquote ( opts [ : root] )
440+ root: unquote ( root )
438441 }
439442 end }
440443 else
@@ -618,20 +621,34 @@ defmodule Phoenix.LiveView.Engine do
618621
619622 defp changed_assigns ( assigns ) do
620623 checks =
621- for { key , _ } <- assigns , not nested_and_parent_is_checked? ( key , assigns ) do
624+ for { { changed_var , key } , _ } <- assigns , not nested_and_parent_is_checked? ( key , assigns ) do
625+ changed = Macro . var ( changed_var , __MODULE__ )
626+
622627 case key do
623628 [ assign ] ->
624629 quote do
625- unquote ( __MODULE__ ) . changed_assign? ( changed , unquote ( assign ) )
630+ unquote ( __MODULE__ ) . changed_assign? ( unquote ( changed ) , unquote ( assign ) )
626631 end
627632
628633 [ assign | tail ] ->
634+ assigns_var =
635+ case changed_var do
636+ :changed ->
637+ @ assigns_var
638+
639+ :vars_changed ->
640+ # we pass a map %{var: var} for nested change tracking
641+ quote do
642+ % { unquote ( assign ) => unquote ( Macro . var ( assign , nil ) ) }
643+ end
644+ end
645+
629646 quote do
630647 unquote ( __MODULE__ ) . nested_changed_assign? (
631648 unquote ( tail ) ,
632649 unquote ( assign ) ,
633- unquote ( @ assigns_var ) ,
634- changed
650+ unquote ( assigns_var ) ,
651+ unquote ( changed )
635652 )
636653 end
637654 end
@@ -700,8 +717,25 @@ defmodule Phoenix.LiveView.Engine do
700717 keys != % { } ,
701718 do: { key , to_component_keys ( keys ) }
702719
720+ has_vars_changed? =
721+ Enum . any? ( keys , fn { _name , entries } ->
722+ is_list ( entries ) and Enum . any? ( entries , & match? ( { :vars_changed , _ } , & 1 ) )
723+ end )
724+
725+ vars_changed =
726+ if has_vars_changed? do
727+ Macro . var ( :vars_changed , __MODULE__ )
728+ else
729+ [ ]
730+ end
731+
703732 quote do
704- unquote ( __MODULE__ ) . to_component_static ( unquote ( keys ) , unquote ( @ assigns_var ) , changed )
733+ unquote ( __MODULE__ ) . to_component_static (
734+ unquote ( keys ) ,
735+ unquote ( @ assigns_var ) ,
736+ changed ,
737+ unquote ( vars_changed )
738+ )
705739 end
706740 else
707741 Macro . escape ( % { } )
@@ -733,14 +767,24 @@ defmodule Phoenix.LiveView.Engine do
733767 true ->
734768 { _ , keys , _ } = analyze_and_return_tainted_keys ( dynamic , vars , % { } , caller )
735769
770+ has_vars_changed? = keys != :all and Enum . any? ( keys , & match? ( { :vars_changed , _ } , & 1 ) )
771+
772+ vars_changed =
773+ if has_vars_changed? do
774+ Macro . var ( :vars_changed , __MODULE__ )
775+ else
776+ [ ]
777+ end
778+
736779 quote do
737780 unquote ( __MODULE__ ) . to_component_dynamic (
738781 % { unquote_splicing ( static ) } ,
739782 unquote ( dynamic ) ,
740783 unquote ( static_changed ) ,
741784 unquote ( to_component_keys ( keys ) ) ,
742785 unquote ( @ assigns_var ) ,
743- changed
786+ changed ,
787+ unquote ( vars_changed )
744788 )
745789 end
746790 end
@@ -750,25 +794,25 @@ defmodule Phoenix.LiveView.Engine do
750794 defp to_component_keys ( map ) , do: Map . keys ( map )
751795
752796 @ doc false
753- def to_component_static ( _keys , _assigns , nil ) do
797+ def to_component_static ( _keys , _assigns , nil , [ ] ) do
754798 nil
755799 end
756800
757- def to_component_static ( keys , assigns , changed ) do
801+ def to_component_static ( keys , assigns , changed , vars_changed ) do
758802 for { assign , entries } <- keys ,
759- changed = component_changed ( entries , assigns , changed ) ,
803+ changed = component_changed ( entries , assigns , changed , vars_changed ) ,
760804 into: % { } ,
761805 do: { assign , changed }
762806 end
763807
764808 @ doc false
765- def to_component_dynamic ( static , dynamic , _static_changed , _keys , _assigns , nil ) do
809+ def to_component_dynamic ( static , dynamic , _static_changed , _keys , _assigns , nil , [ ] ) do
766810 merge_dynamic_static_changed ( dynamic , static , nil )
767811 end
768812
769- def to_component_dynamic ( static , dynamic , static_changed , keys , assigns , changed ) do
813+ def to_component_dynamic ( static , dynamic , static_changed , keys , assigns , changed , vars_changed ) do
770814 component_changed =
771- if component_changed ( keys , assigns , changed ) do
815+ if component_changed ( keys , assigns , changed , vars_changed ) do
772816 Enum . reduce ( dynamic , static_changed , fn { k , _ } , acc -> Map . put ( acc , k , true ) end )
773817 else
774818 static_changed
@@ -781,19 +825,23 @@ defmodule Phoenix.LiveView.Engine do
781825 dynamic |> Map . merge ( static ) |> Map . put ( :__changed__ , changed )
782826 end
783827
784- defp component_changed ( :all , _assigns , _changed ) , do: true
828+ defp component_changed ( :all , _assigns , _changed , _vars_changed ) , do: true
785829
786- defp component_changed ( [ path ] , assigns , changed ) do
830+ defp component_changed ( [ path ] , assigns , changed , vars_changed ) do
787831 case path do
788- [ key ] -> changed_assign ( changed , key )
789- [ key | tail ] -> nested_changed_assign ( tail , key , assigns , changed )
832+ { :changed , [ key ] } -> changed_assign ( changed , key )
833+ { :changed , [ key | tail ] } -> nested_changed_assign ( tail , key , assigns , changed )
834+ { :vars_changed , [ key ] } -> changed_assign ( vars_changed , key )
835+ { :vars_changed , [ key | tail ] } -> nested_changed_assign ( tail , key , assigns , vars_changed )
790836 end
791837 end
792838
793- defp component_changed ( entries , assigns , changed ) do
839+ defp component_changed ( entries , assigns , changed , vars_changed ) do
794840 Enum . any? ( entries , fn
795- [ key ] -> changed_assign? ( changed , key )
796- [ key | tail ] -> nested_changed_assign? ( tail , key , assigns , changed )
841+ { :changed , [ key ] } -> changed_assign? ( changed , key )
842+ { :changed , [ key | tail ] } -> nested_changed_assign? ( tail , key , assigns , changed )
843+ { :vars_changed , [ key ] } -> changed_assign? ( vars_changed , key )
844+ { :vars_changed , [ key | tail ] } -> nested_changed_assign? ( tail , key , assigns , vars_changed )
797845 end )
798846 end
799847
@@ -887,11 +935,29 @@ defmodule Phoenix.LiveView.Engine do
887935 { ast , keys , vars }
888936 end
889937
938+ # if we find a variable (or something more complex handled by the other clauses)
939+ # like foo[:bar][:baz] and foo is marked as :change_track in vars, we consider it
940+ # as an assign, but look into vars_changed instead of changed
941+ defp analyze_assign (
942+ { name , _ , context } = expr ,
943+ { type , map } = vars ,
944+ assigns ,
945+ _caller ,
946+ nest
947+ )
948+ when is_atom ( name ) and is_atom ( context ) and is_map_key ( map , name ) and type != :tainted do
949+ if map [ name ] == :change_track do
950+ { expr , vars , Map . put ( assigns , { :vars_changed , [ name | nest ] } , true ) }
951+ else
952+ { expr , vars , assigns }
953+ end
954+ end
955+
890956 # @name
891957 defp analyze_assign ( { :@ , meta , [ { name , _ , context } ] } , vars , assigns , _caller , nest )
892958 when is_atom ( name ) and is_atom ( context ) do
893959 expr = { { :. , meta , [ @ assigns_var , name ] } , [ no_parens: true ] ++ meta , [ ] }
894- { expr , vars , Map . put ( assigns , [ name | nest ] , true ) }
960+ { expr , vars , Map . put ( assigns , { :changed , [ name | nest ] } , true ) }
895961 end
896962
897963 # assigns.name
@@ -903,7 +969,7 @@ defmodule Phoenix.LiveView.Engine do
903969 nest
904970 )
905971 when is_atom ( name ) and args in [ [ ] , nil ] do
906- { expr , vars , Map . put ( assigns , [ name | nest ] , true ) }
972+ { expr , vars , Map . put ( assigns , { :changed , [ name | nest ] } , true ) }
907973 end
908974
909975 # assigns[:name]
@@ -915,7 +981,7 @@ defmodule Phoenix.LiveView.Engine do
915981 nest
916982 )
917983 when is_atom ( name ) and is_access ( access ) do
918- { expr , vars , Map . put ( assigns , [ name | nest ] , true ) }
984+ { expr , vars , Map . put ( assigns , { :changed , [ name | nest ] } , true ) }
919985 end
920986
921987 # Maybe: assigns.foo[:bar]
@@ -984,20 +1050,39 @@ defmodule Phoenix.LiveView.Engine do
9841050 { expr , vars , assigns }
9851051 end
9861052
987- # Vars always taint unless we are in restricted mode.
988- defp analyze ( { name , meta , nil } = expr , { :restricted , map } , assigns , caller )
1053+ # Vars always taint unless we are in restricted mode
1054+ # or the variable is marked as `:change_track` for vars_changed.
1055+ defp analyze ( { name , meta , nil } = expr , { :restricted , map } = vars , assigns , caller )
9891056 when is_atom ( name ) do
990- if Map . has_key? ( map , name ) do
991- maybe_warn_taint ( name , meta , caller )
992- { expr , { :tainted , map } , assigns }
993- else
994- { expr , { :restricted , map } , assigns }
1057+ case map do
1058+ % { ^ name => :tainted } ->
1059+ maybe_warn_taint ( name , meta , caller )
1060+ { expr , { :tainted , map } , assigns }
1061+
1062+ % { ^ name => :change_track } ->
1063+ { expr , vars , Map . put ( assigns , { :vars_changed , [ name ] } , true ) }
1064+
1065+ _ ->
1066+ { expr , { :restricted , map } , assigns }
9951067 end
9961068 end
9971069
998- defp analyze ( { name , meta , nil } = expr , { _ , map } , assigns , caller ) when is_atom ( name ) do
999- maybe_warn_taint ( name , meta , caller )
1000- { expr , { :tainted , Map . put ( map , name , true ) } , assigns }
1070+ defp analyze ( { name , meta , nil } = expr , { type , map } , assigns , caller )
1071+ when is_atom ( name ) do
1072+ cond do
1073+ Map . get ( map , name ) == :change_track ->
1074+ { expr , { type , map } , Map . put ( assigns , { :vars_changed , [ name ] } , true ) }
1075+
1076+ Keyword . get ( meta , :change_track ) ->
1077+ # this is a variable inside the left-hand side of a keyed for expression;
1078+ # we mark it as change_track in the vars map so that we treat it as change-tracked
1079+ # when we see it used again later (see the previous analyze clause above)
1080+ { expr , { type , Map . put ( map , name , :change_track ) } , assigns }
1081+
1082+ true ->
1083+ maybe_warn_taint ( name , meta , caller )
1084+ { expr , { :tainted , Map . put ( map , name , :tainted ) } , assigns }
1085+ end
10011086 end
10021087
10031088 # Quoted vars are ignored as they come from engine code.
@@ -1334,8 +1419,11 @@ defmodule Phoenix.LiveView.Engine do
13341419 defp classify_taint ( :with , [ _ | _ ] ) , do: :live
13351420 defp classify_taint ( :for , [ _ | _ ] ) , do: :live
13361421
1337- # Constructs from Phoenix and TagEngine
1422+ # Constructs from TagEngine
13381423 defp classify_taint ( :inner_block , [ _ , [ do: _ ] ] ) , do: :live
1424+ defp classify_taint ( :keyed_comprehension , [ _ , _ , [ do: _ ] ] ) , do: :live
1425+
1426+ # Constructs from Phoenix.View
13391427 defp classify_taint ( :render_layout , [ _ , _ , _ , [ do: _ ] ] ) , do: :live
13401428
13411429 # Special forms are forbidden and raise.
0 commit comments