You are observing the results from what has not been standardized with POSIX.
POSIX does not standarize how the interpreter runs a pipe.
With the historic Bourne Shell, the rightmost program in a pipeline is not even a child of the main shell. The reason for doing it this way is because this implementation is slow but needs few code - important if you only have 64 kB of memory. Since in this variant, the
read command is run in a sub process, assigning a shell variable in a sub process is not visible in the main shell.
Modern shells like
bosh (the recent Bourne Shell) create pipes in a way where all processes in a pipe are direct children of the main shell and if the rightmost program is a shell builtin, it is even run by the main shell.
This all is needed to let the
read program modify a shell variable of the main shell. So only in this variant (that BTW is the fastest variant) permits the main shell to see the results from the variable assignment.
In your second example, the whole
while loop is run in the same sub process and thus allows to print the modified version of the shell variable.
There is curently a request to add support to the POSIX shell to get information on whether any of the pipeline commands has a non-zero exit code. To achieve this, a shell must be implemented in a way where all programs from a pipeline are direct children from the main shell. This is close to what is needed to allow
echo foo | read val; echo $val
to do what is expected, since then only the requirement to run the read in the main shell is missing.