Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Setting instance level attributes in Reflex as a way to set context data is a confusing API #122

Open
dylanjw opened this issue Jul 4, 2021 · 20 comments · May be fixed by #136
Open

Setting instance level attributes in Reflex as a way to set context data is a confusing API #122

dylanjw opened this issue Jul 4, 2021 · 20 comments · May be fixed by #136

Comments

@dylanjw
Copy link

dylanjw commented Jul 4, 2021

Feature Request

Background:
I want to start out by saying Im new to sockpuppet, but love this project. One thing though. I find setting instance level attributes in Reflexes as a way to update context data to be a confusing API

Related to issue #43, It would be nice to make the documentation more explicit that user added reflex attributes get propagated into the context data during refreshes.

I also find the existence of the context attribute in reflexes promotes this confusion. For myself and other uninitiated, setting context in a reflex instance has surprising behavior:

  1. The context attribute is only populated with data after calling get_context_data(), and setting the context attribute overrides the data returned by get_context_data.
>>> reflex.context
{}
>>> reflex.get_context_data()
{'stimulus_reflex': True, 'view': <app.views.NewView object at 0x7ff32cdb36d0>}
>>> reflex.context
{'stimulus_reflex': True, 'view': <app.views.NewView object at 0x7ff32cdb36d0>}
>>> reflex.context = {"field2": "value2"}
>>> reflex.get_context_data()
{'field2': 'value2'}
  1. Fields added to the context attribute in the reflex do not get passed to the template and view.

Possible solutions

Editing the documentation to better explain the API for changing context data would be a first step. A further improvement could be made to the overall API in one of two ways:

  1. Make context "private" by prepending with a "_". Right now it looks like its part of the API and not something reserved for internal use.
  2. Modify behavior where context data is pulled from reflex instance attributes, and instead update/merge with data from context.
class MyReflex(Reflex):
    def update(self):
        self.field = "data"

becomes

class MyReflex(Reflex):
    def update(self):
        self.context.field = "data"

I think option 2 is a better direction but it would be a breaking change. It would be ideal to release a minor version where context is pulled both from the context attribute and instance level attributes before phasing out the instance level attribute propagation in a major version.

Maybe I am confusing some of the intended API and behavior. Would love to hear others thoughts.

PR link

TBD

@jonathan-s
Copy link
Owner

Thank @dylanjw for opening a thoughtful issue. I agree that editing the documentation to become clearer is definitely a good first step. It should definitely be mentioned in clearer terms on the reflex section in the documentation. If you've got suggestions to where you would like to see it elsewhere I'm open to hear it.

What I had done earlier is to exclude "protected variables" > https://github.com/jonathan-s/django-sockpuppet/blob/master/sockpuppet/consumer.py#L237, however I had missed to add context to that. I guess it would be useful make a warning in the commandline if the user tries to add variables that are protected, mentioning context more clearly in the documentation should be done as well. Making context a private variable is also a good idea.

With that out of the way it should become less confusing for a user, or would you still find it confusing? Perhaps @nerdoc or @DamnedScholar have some thoughts as well?

@DamnedScholar
Copy link
Contributor

What if Reflex.get_context_data() set to Reflex._original_context (removing the part where it aborts fetching the original context if there's already a context) and Reflex.context had a setter that set to Reflex._new_context? Then Reflex.context could have a getter that returns Reflex._original_context.update(self._new_context) and the original context would never get overridden on accident.

I do think it makes sense to only pass variables under context to the context. It's a little more wordy, less magical, more explicit. It also provides a cognitive separation between your product (what's going to the template) and your workspace (the Reflex and its variables).

@jonathan-s
Copy link
Owner

Making context a property in the first place would also make impossible to write over. So that would be another safe-guard.

@dylanjw
Copy link
Author

dylanjw commented Jul 13, 2021

I'd like to make another argument in support of namespacing passed context variables. Extending the reflex class with new methods/attributes has the potential to be a breaking change for someone with conflicting variable names in their project. While moving over to using context for passing variables is a bit of a pain now, it sets up the reflex class to be more easily extensible in the future.

@dylanjw
Copy link
Author

dylanjw commented Jul 13, 2021

I agree that editing the documentation to become clearer is definitely a good first step. It should definitely be mentioned in clearer terms on the reflex section in the documentation. If you've got suggestions to where you would like to see it elsewhere I'm open to hear it.

I can open a PR with some doc changes.

@jonathan-s
Copy link
Owner

I can open a PR with some doc changes.

That would be most welcome :)

@dylanjw
Copy link
Author

dylanjw commented Jul 13, 2021

I would also be into making the changes as @DamnedScholar suggested, to see what those could look like and keep the discussion going.

@nerdoc
Copy link
Contributor

nerdoc commented Jul 13, 2021

I don't know if I understood correctly. But this is one of the thins tat are unnecessarily complicated in Sockpuppet IMHO. First, at the template, you have to set the data-<variable>. and then it is available in the Reflex, as I understood. This creates boilerplate code like the one at https://sockpuppet.argpar.se/quickstart-django:

    <a href="#"
    data-reflex="click->CounterReflex#increment"
    data-step="1"
    data-count="{{ count }}"
    >Increment {{ count }}</a>

Why is the data-value needed in the first place?

On the backend side, you need to reference self.element["data-count"] here, scroll down to the 'Untitled' code fragment to get the data. (or self.element.dataset['count'] here? Confusing!

from sockpuppet.reflex import Reflex

class CounterReflex(Reflex):
    def increment(self):
        self.count = (
            int(self.element.dataset['count']) +
            int(self.element.dataset['step'])
        )

It would be much more pythonic to write:

from sockpuppet.reflex import Reflex

class CounterReflex(Reflex):
    # define class variables here, which become automatically 
    # available as template context variables in the frontend
    count = 0
    step = 1

    def increment(self):
        self.count += self.step

At the template, this would be

    <a href="#"
    data-reflex="click->CounterReflex#increment"
    >Increment {{ count }}</a>

and with available parameters:

    <a href="#"
    data-reflex="click->CounterReflex#increment(1)"
    >Increment {{ count }}</a>
    def increment(self, step):

or even dynamically:

    <a href="#"
    data-reflex="click->CounterReflex#increment({{ step }})"
    >Increment {{ count }}</a>
class CounterReflex(Reflex):
    count = 0
    step = 1

    def increment(self, inc):
        # the "inc" param here in this case gets filled with the {{step}} variable again. Just to show a use case.
        self.count += inc
        if inc == 1:
            self.step = 2
        else:
            self.step=1

E.g. Unicorn does it linke this, and it is MUCH simpler, and let's the user set instance variables of the class (here Reflex) which automatically become context variables in the reflex' template.

If I would design an API, I'd do it like this...

So @dylanjw, if I understood correctly, this (instance variables are context variables?) is already the current behavior in SP and you would like to change it?
I think the main purpose of designing an API is keeping it simple to understand. Everything else makes it prone to errors on the long term...

@nerdoc
Copy link
Contributor

nerdoc commented Jul 13, 2021

The data-count={{count}} attr ATM is needed to get the value back to the server. Unicorn uses the unicorn:model attribute for that, much like Vue.js does.
But even if you keep the data-<...> attributes for explicit copying values to the backend, on the Reflex side, it would be easier to use instance/class (?) attributes to deal with the values, instead of self.element.dataset*stuff

@nerdoc
Copy link
Contributor

nerdoc commented Jul 13, 2021

Extending the reflex class with new methods/attributes has the potential to be a breaking change for someone with conflicting variable names in their project. While moving over to using context for passing variables is a bit of a pain now, it sets up the reflex class to be more easily extensible in the future.

Ah ok, now I understand better. Using context is for sure a way to go to not break existing context variables. But again: Unicorn solves this in a brilliant way IMHO: it encapsulates the "component" more than SP: you have a UnicornView which is basically a Reflex, and each of these views has it's own template which renders the "reflex". Context variables are only available within this template.
You can then render these components using a templatetag {% unicorn 'my-component' %} This solves the namespace problem and context var clashing IMHO better.

@dylanjw
Copy link
Author

dylanjw commented Jul 13, 2021

Im just concerned with the API for passing context variables from the Reflex class to the client. Getting data back from the client to the Reflex is a bit awkward, but is out of scope for the changes being proposed.

This might really just come down to personal opinion on API design. It feels like the Reflex is mixing concerns by putting context variables to be sent to the client as instance attributes. The Reflex is acting as both an object representing a UI interaction and a special datatype for the template context. It feels weird as a user to make sure my instance methods don't clash with template context variables, and also have to work around having any instance variables I add not just internal to the working of the class/instance but also potentially passed to the client. Putting the context variables under context would feel cleaner to me.

@jonathan-s
Copy link
Owner

jonathan-s commented Jul 14, 2021

@dylanjw Having slept on this I think the api that you describe makes sense and the arguments you make are sound. Ie

class MyReflex(Reflex):
    def update(self):
        self.context.field = "data"

It should be backwards compatible, so that we can remove the old behaviour in a major version. We should also leave a logging warning if the new api isn't used so that the developer is notified in the command prompt.

@nerdoc
Copy link
Contributor

nerdoc commented Jul 14, 2021

Ok - I see this is clearly the better solution, and @dylanjw you're right it's a point of view - how API designing is preferred. In this case, as Reflexes always are in context of a whole view, I definitely opt for separation of context as well... ❤️

@nerdoc
Copy link
Contributor

nerdoc commented Jul 22, 2021

I've slept a few nights on this topic again, and something else occurred to me.

self.context.field = "data"

is great for the writing side of data access. But what about reading? I think this is very confusing as well.
If you just want to read an input element's value, you have to write:

name = self.element["attribute"]["value"] 

BTW: according to the docs, the element attribute should have these attributes directly ("The element property contains all of the Reflex controller's DOM element attributes as well as other properties like, tagName, checked and value."), which it doesn't:
image
self.element contains 'attributes, which contains a valuedict key. No properties in [Element](https://github.com/jonathan-s/django-sockpuppet/blob/master/sockpuppet/element.py) exceptdataset`, and the attributes dict.

{
  'reflex': 'change->MyReflex#update', 
  'controller': 'stimulus-reflex', 
  'action': 'change->stimulus-reflex#__perform'
}

which is totally confusing IMHO, and badly documented.

In 99% of all cases I suppose that the user wants to access the "value" of the input field. the element accessor is some sort of low level api (with access to all html attributes etc).

So my suggestion here is something else: Why can't SP provide a higher level API for data reading access?

SP could bind e.g. the value to a value property:

class MyReflex(Reflex):
    def increment(self, step: int = 1):
        name = self.element.value  # shortcut for self.element["attributes"]["value"]
        self.context.upper_name = name.upper()

This would be an approach to get data more easily out of the elements. It's much more readable and pythonic.
Please tell me what you think of that.

I could be something like this in the Element's code:

    @property
    def value(self):
        return self.attributes["value"]

    @property
    def checked(self):
        return self.attributes["checked"]

@jonathan-s
Copy link
Owner

This would be an approach to get data more easily out of the elements. It's much more readable and pythonic.
Please tell me what you think of that.

That would totally work for me. However it's worth keeping in mind that you can add any attribute to html so doing it through properties as above is probably not the best solution.

@nerdoc
Copy link
Contributor

nerdoc commented Jul 22, 2021

... that you can add any attribute to html so doing it through properties as above is probably not the best solution.

That's true. I didn't want to suggest replacing the low-level API with properties. It's just that e.g. value, checked, selectedetc are attrs that are used very often and make sense to have easy accessors. Would make sense to get sane defaults instead of Exceptions too, like when `element.attribute["value"] could raise an Exception when there is no value? The property could just return a "".

@nerdoc
Copy link
Contributor

nerdoc commented Jul 22, 2021

Ah, and it's not necessary to do hardcoded properties. Could be a __getattr__ generic one.

@jonathan-s
Copy link
Owner

jonathan-s commented Jul 23, 2021

I think it would also be preferable to keep the api the same as javascript ie for an <a> element with a href attribute it would be the following, and it should stay backwards compatible for the time being.

element.attributes.href

That way it would be consistent. I would also say it would be good to keep this in two separate PRs for the person who takes this on. Just checking in @dylanjw, were you keen on creating a PR for the context.

@nerdoc
Copy link
Contributor

nerdoc commented Jul 23, 2021

ok - but then there's the docs - ATM they are wrong IMHO, even for the current behaviour. as said, "The element property contains all of the Reflex controller's DOM element attributes as well as other properties like, tagName, checked and value.",

This should (ATM) be: "The element property contains an attribute dict which contains all of the Reflex controllers DOM element attributes as well as other properties like tag_name, checked and value."
Mind that "tagName" is completely wrong.

Would be a very small, but important docs improvement, which costs much time for beginners (like me).

jonathan-s added a commit that referenced this issue Jul 24, 2021
@nerdoc pointed out that the element documentation was wrong
in #122, so this updates the documentation to the current
behaviour.
@jonathan-s
Copy link
Owner

@dylanjw FYI, I committed a documentation update here: 76144ed how it works to set a context right now. This will then be updated in #136.

Danilodum added a commit to MedianaSoftware/django-sockpuppet that referenced this issue Aug 20, 2021
commit 5a6b5781a792eda242ce7608314b19edaac78ee1
Merge: bf37de9 81a0fbf
Author: Danilodum <[email protected]>
Date:   Fri Aug 20 14:42:31 2021 +0200

    Merge branch 'master' of https://github.com/jonathan-s/django-sockpuppet into jonathan-s-master

commit 81a0fbf
Author: Jonathan Sundqvist <[email protected]>
Date:   Wed Aug 4 20:04:42 2021 +0200

    Some Channel improvements (jonathan-s#139)

    - Allow channel methods to be chained
    - Improve the doc strings with updated docs from cable ready.
    - Allow keyword arguments for each method that will be used in options.

commit 728eb9c
Author: Jonathan Sundqvist <[email protected]>
Date:   Sat Jul 31 22:04:40 2021 +0200

    Emit browser console error when a reflex can't be loaded (jonathan-s#138)

commit 76144ed
Author: Jonathan Sundqvist <[email protected]>
Date:   Sat Jul 31 16:31:17 2021 +0200

    Create a clearer description of modifying context in a reflex

commit 23f5bf4
Author: GitHub Actions <[email protected]>
Date:   Sat Jul 24 09:38:55 2021 +0000

    [nodoc] Update Changelog

commit bc86e10
Author: Jonathan Sundqvist <[email protected]>
Date:   Sat Jul 24 11:38:26 2021 +0200

    Remove Channels 2.4 in CI (jonathan-s#134)

    Channels 2.4 is proving problematic in CI, and it's not worth
    the effort to maintain that. Sockpuppet should still work for
    channels 2.4, but given we don't run a CI for it we can't make
    any promises for it.

commit 52e3209
Author: Jonathan Sundqvist <[email protected]>
Date:   Sat Jul 24 11:19:31 2021 +0200

    Amend element description to be correct

    @nerdoc pointed out that the element documentation was wrong
    in jonathan-s#122, so this updates the documentation to the current
    behaviour.

commit 695ee72
Author: GitHub Actions <[email protected]>
Date:   Mon Jul 19 19:48:15 2021 +0000

    [nodoc] Update Changelog

commit 153e5b0
Author: ciag00 <[email protected]>
Date:   Mon Jul 19 20:47:35 2021 +0100

    Fixing running initial_sockpuppet in windows (fixes jonathan-s#106) (jonathan-s#132)

commit 455c71a
Author: GitHub Actions <[email protected]>
Date:   Wed Jul 14 06:39:11 2021 +0000

    [nodoc] Update Changelog

commit c8140ae
Author: Jonathan Sundqvist <[email protected]>
Date:   Wed Jul 14 08:38:37 2021 +0200

    CI for django 3.2 (jonathan-s#127)

commit 86f649e
Author: GitHub Actions <[email protected]>
Date:   Wed Jul 14 06:13:03 2021 +0000

    [nodoc] Update Changelog

commit 51adb6b
Author: Jonathan Sundqvist <[email protected]>
Date:   Wed Jul 14 08:12:31 2021 +0200

    Don't use exception, instead of figure out what view it is. (jonathan-s#124)

    Figuring out which view it is and adding the correct property to
    that view is better than trying to re-render the view itself.

    This should solve for django's generic views. It won't work
    for Django vanilla views package, but we can deal with this
    later if someone show interest for that.

commit 09375f9
Author: GitHub Actions <[email protected]>
Date:   Wed Jul 14 05:59:07 2021 +0000

    [nodoc] Update Changelog

commit dc246ac
Author: Jonathan Sundqvist <[email protected]>
Date:   Wed Jul 14 07:58:36 2021 +0200

    Bump dependencies (jonathan-s#126)

    Bump ws from 6.2.1 to 6.2.2
    Bump browserslist from 4.11.1 to 4.16.6
    Bump hosted-git-info from 2.8.5 to 2.8.9
    Bump lodash from 4.17.20 to 4.17.21

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit 1cc3865
Author: GitHub Actions <[email protected]>
Date:   Tue Jul 13 20:57:56 2021 +0000

    [nodoc] Update Changelog

commit a65444e
Author: Jonathan Sundqvist <[email protected]>
Date:   Tue Jul 13 22:57:23 2021 +0200

    Make the code black (jonathan-s#125)

commit 69047da
Author: GitHub Actions <[email protected]>
Date:   Tue Jul 13 20:54:52 2021 +0000

    [nodoc] Update Changelog

commit c33e021
Author: Andros Fenollosa <[email protected]>
Date:   Tue Jul 13 22:54:19 2021 +0200

    Minor fix (jonathan-s#121)

commit 4b7bcd7
Author: GitHub Actions <[email protected]>
Date:   Tue Jul 13 19:33:10 2021 +0000

    [nodoc] Update Changelog

commit 2ff7fd4
Author: Christian González <[email protected]>
Date:   Tue Jul 13 21:32:38 2021 +0200

    Making camelCase and PascalCase more robust (jonathan-s#87)

    * Use Django's newer path method instead of url
    * gitignore sockpuppet/static/
    * improve test README + enable pytest as test suite
    * add camelcase/pascacase templatetags filters
    * lint: remove whitespace + unused impotr
    * lint: fix whitespace
    * fix double pascalizing  Reflex classes
    * fix linter error, remove unused import
    * validate reflex_name and keep it clear during scaffolding lifecycle
    * use f strings instead of format
    * use wording of other code doc
    * fix li index in doc
    * remove gitignored static files
    * make camelcase function more robust
    it now preserves already camelcased strings
    * remove unused (commented out) tests

commit 79cc9da
Author: GitHub Actions <[email protected]>
Date:   Tue Jul 13 19:28:51 2021 +0000

    [nodoc] Update Changelog

commit 1639aa7
Author: Christian González <[email protected]>
Date:   Tue Jul 13 21:28:22 2021 +0200

    extract/const isProd (jonathan-s#118)

    to satisfy ESLint
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants