Editor: Implement meta as custom source#16402
Conversation
| */ | ||
| export function resetEditorBlocks( blocks, options = {} ) { | ||
| export function* resetEditorBlocks( blocks, options = {} ) { | ||
| for ( const name in sources ) { |
There was a problem hiding this comment.
I'm not sure if it's an issue with Babel transpilation or a distinct behavior for how named exports "objects" are iterated, but I would have preferred to use a more concise for ( const source of sources ) { syntax here, yet I was encountering runtime errors ¯\_(ツ)_/¯
There was a problem hiding this comment.
Objects are not Iterable. You would need to for of Object.keys/values/entries/etc(sources) or export an array of sources.
There was a problem hiding this comment.
This would be a good place to call an updateAll if it exists.
There was a problem hiding this comment.
Objectsare notIterable. You would need tofor of Object.keys/values/entries/etc(sources)or export an array of sources.
Oh! That seems obvious in retrospect. I guess for ( const source of Object.values( sources ) ) ) { might be what I'm looking for here then.
|
|
||
| if ( onBlockAttributesChange ) { | ||
| const [ clientId, attributes ] = newLastBlockAttributesChange; | ||
| onBlockAttributesChange( clientId, attributes ); |
There was a problem hiding this comment.
Very first thought :) Seeing this made me wonder about an idea. Not sure yet how valuable it is or if it will allow us to improve things. but this callback could serve as a way to make the updates the blocks and "return" them.
I didn't read the PR completely but If I'm not mistaken this callback forces the caller to "set blocks" twice. When this callback is called and when onInput is called too?
(I might be completely wrong here, as I didn't dive yet)
There was a problem hiding this comment.
I didn't read the PR completely but If I'm not mistaken this callback forces the caller to "set blocks" twice. When this callback is called and when
onInputis called too?
No, because onBlockAttributesChange isn't actually applying anything to the blocks, it's only triggering the side effect (updating the post). As you mention, it's the subsequent onChange / onInput which calls resetBlocks, at which point those values are reflected in all impacted blocks.
There was a problem hiding this comment.
No, because
onBlockAttributesChangeisn't actually applying anything to the blocks, it's only triggering the side effect (updating the post). As you mention, it's the subsequentonChange/onInputwhich callsresetBlocks, at which point those values are reflected in all impacted blocks.
An unfortunate consequence of this behavior: Since we rely on the subsequent resetBlocks and there's nothing about editPost itself which immediately triggers an application of sourced attribute values, if someone were to call editPost directly with updated meta values, the current implementation would not immediately update blocks which derive from this meta.
There was a problem hiding this comment.
Can we make this part of the onChange/onInput actions instead of adding another action?
There was a problem hiding this comment.
An unfortunate consequence of this behavior: Since we rely on the subsequent resetBlocks and there's nothing about editPost itself which immediately triggers an application of sourced attribute values, if someone were to call editPost directly with updated meta values, the current implementation would not immediately update blocks which derive from this meta.
We need a way for custom sources to subscribe to events/actions that should re-apply their values.
| @@ -0,0 +1,41 @@ | |||
| BlockEditorProvider | |||
There was a problem hiding this comment.
❤️ I love this README :)
This prompted me to consider that we likely have a problem here with how the BlockEditorProvider considers its value as canonical, and upon an "outbound" sync will ignore the value provided in the next render. This could perhaps explain the issues with other meta blocks not being updated immediately. Your idea here could work at least for the one block being updated, but doesn't account for other meta blocks which source from the same meta property. In another of my experimental branches, I had considered whether it would be enough that the BlockEditorProvider ensure the next value it receives in a sync matches what it had expected based on what was sent. We could use this in the implementation here, then only "apply" values to blocks if they differ from what was sent by the block editor, thus triggering the BlockEditorProvider to reset its blocks based on what was applied from the editor. Admittedly, this is one thing the alternative considered proposal might handle well, at least so far as the block editor doesn't need to become aware of this new value resulting from a sync, since it retains the role of being the canonical source of values. |
|
In thinking again about the "Source API" here, and partly with regard to my previous comment at least so far as the added complexity in avoiding to mutate (and only create new references) for blocks "applied", I wonder if we could eliminate In other words:
This way, the framework can decide whether the value from The downside here is that we revert back to a previously-mentioned potential performance concern. I think this can be mitigated by the fact we only run |
|
From #16402 (comment):
We could solve this in a similar way to #16075 by having built-in awareness to Considering how it might be made to be generic, the sources would need some way to subscribe to the store, and in the case of meta, receive dependent data upon whose changing should cause a re-application of sourced data on blocks. |
epiqueras
left a comment
There was a problem hiding this comment.
I am really liking this approach. Props on coming up with such a clean way to introduce it. 👏
I think the only unsolved problem is:
We need a way for custom sources to subscribe to events/actions that should re-apply their values.
So that things like direct editPost calls don't throw things out of sync. Maybe custom sources can export something like:
export const reApplyOn = [ 'EDIT_POST' ]| * **Type:** `Function` | ||
| * **Required** `no` | ||
|
|
||
| A callback invoked when the blocks have been modified in a persistent manner. Contrasted with `onInput`, a "persistent" change is one which is not an extension of a composed input. Any update to a distinct block or block attribute is treated as persistent. |
There was a problem hiding this comment.
...change is one which is not an extension of a composed input...
This is a bit hard to grasp. Maybe an example would help.
| * **Type:** `Function` | ||
| * **Required** `no` | ||
|
|
||
| A callback invoked when the blocks have been modified in a non-persistent manner. Contrasted with `onChange`, a "non-persistent" change is one which is part of a composed input. Any sequence of updates to the same block attribute are treated as non-persistent, except for the first. |
There was a problem hiding this comment.
Any sequence of updates to the same block attribute are treated as non-persistent, except for the first.
Why is this desired? If I edit a color twice, the second edit wouldn't persist?
There was a problem hiding this comment.
Why is this desired? If I edit a color twice, the second edit wouldn't persist?
I think the choice of "persistent" as the term here can be potentially misleading. The intent was for it to be akin to the distinction between input and change events in the DOM API:
The input event is fired every time the value of the element changes. This is unlike the change event, which only fires when the value is committed, such as by pressing the enter key, selecting a value from a list of options, and the like.
https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event
https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event
In the context of the editor, the use-case is for Undo levels. You don't want Cmd+Z to undo paragraph changes one character at a time, but rather as units (in our case, reflected as sequences of updates to the same block attribute).
When I was first thinking about this documentation, I had in mind to explicitly mention how it's used for Undo/Redo in the editor, but neglected to write it when I'd returned to my computer 😄
There was a problem hiding this comment.
Thanks, that's much clearer now. I remember running into that code, but forgot about it when reading this.
| ### `value` | ||
|
|
||
| * **Type:** `Array` | ||
| * **Required** `no` |
There was a problem hiding this comment.
The component will try to use all of these props (except for "children") and throw errors. We should mark them as required.
There was a problem hiding this comment.
The component will try to use all of these props (except for "children") and throw errors. We should mark them as required.
I was actually thinking we should make them optional. At least in the case of onInput and onChange, it's quite likely most usage will only care to provide one or the other.
I agree that the documentation here is not accurate per the current implementation. I didn't want to document an undesirable requirement, however. Maybe we should seek to make it optional as a separate task to this pull request.
|
|
||
| if ( onBlockAttributesChange ) { | ||
| const [ clientId, attributes ] = newLastBlockAttributesChange; | ||
| onBlockAttributesChange( clientId, attributes ); |
There was a problem hiding this comment.
Can we make this part of the onChange/onInput actions instead of adding another action?
| /** | ||
| * Reducer return an updated state representing the most recent block attribute | ||
| * update. The state is structured as a tuple of the clientId of the block and | ||
| * the partial object of updated attributes values. |
There was a problem hiding this comment.
Reducer return an
updated attributes values
Typos, maybe?
|
|
||
| if ( onBlockAttributesChange ) { | ||
| const [ clientId, attributes ] = newLastBlockAttributesChange; | ||
| onBlockAttributesChange( clientId, attributes ); |
There was a problem hiding this comment.
An unfortunate consequence of this behavior: Since we rely on the subsequent resetBlocks and there's nothing about editPost itself which immediately triggers an application of sourced attribute values, if someone were to call editPost directly with updated meta values, the current implementation would not immediately update blocks which derive from this meta.
We need a way for custom sources to subscribe to events/actions that should re-apply their values.
|
|
||
| for ( const [ attributeName, schema ] of Object.entries( blockType.attributes ) ) { | ||
| if ( attributes.hasOwnProperty( attributeName ) && sources[ schema.source ] ) { | ||
| yield* sources[ schema.source ].update( schema, attributes[ attributeName ] ); |
There was a problem hiding this comment.
Nice use of generator delegation!
| */ | ||
| export function resetEditorBlocks( blocks, options = {} ) { | ||
| export function* resetEditorBlocks( blocks, options = {} ) { | ||
| for ( const name in sources ) { |
There was a problem hiding this comment.
Objects are not Iterable. You would need to for of Object.keys/values/entries/etc(sources) or export an array of sources.
| */ | ||
| export function resetEditorBlocks( blocks, options = {} ) { | ||
| export function* resetEditorBlocks( blocks, options = {} ) { | ||
| for ( const name in sources ) { |
There was a problem hiding this comment.
This would be a good place to call an updateAll if it exists.
| @@ -0,0 +1,22 @@ | |||
| Block Sources | |||
Maybe. @youknowriad mentioned a specific concern to me that this might help address; namely, because we currently have the source updating and reset and separate actions, there's technically a state which is out of sync between those two actions. This would not be directly reachable by any user interaction, but is not desirable from purely a data perspective. He'd incorporated the block updates into the reset step in his work of #16075. The main concern I have with this is that we expand the responsibilities of a reset action (normally just a setter) to become more aware of the cause of those changes, which to me is still a two-stage process, except bundled into a single action. |
That looks like it would fix this issue.
Yeah that makes more sense now that we need that equality check in the block editor provider
Something along the lines of: export const reApplyOn = [ 'EDIT_POST' ]
// Also:
export const reApplyOn = { 'EDIT_POST': ( state, action ) => true || false } |
Yeah, having it in one action would be better. |
|
In speaking directly with @epiqueras , we mentioned a few ideas for revisions. Per prior comments #16402 (comment), #16402 (comment), and #16402 (review), we need some way to subscribe to changes. I think this could also be used as a way to provide "shared" resources in applying attributes to the blocks, which is currently what the proposed Sample interface: export function* getDependencies() {}
export function* apply( attributeSchema, dependencies ) {}
export function* update( attributeSchema, value ) {}Meta example: export function* getDependencies() {
return { meta: yield select( 'core/editor', 'getEditedPostAttribute', 'meta' ) };
}
export function apply( attributeSchema, { meta } ) {
return meta[ attributeSchema.meta ];
}
export function* update( attributeSchema, value ) {
yield editPost( { meta: { [ attributeSchema.meta ]: value } } );
}Per comments #16402 (comment) and #16402 (comment), we may want to explore incorporating awareness of the specific block updates which contributed to Per comment #16402 (comment), we should incorporate this fix to assure that when the BlockEditorProvider performs an "outbound sync", the value it receives next is what it assumes it should be, and reset if it is different (in the case of meta updates, we will produce a new value reference if there are multiple meta blocks which need to be updated in response to the change). |
|
From exploring with @youknowriad how this would work with a post-content block that uses inner blocks. We think we need a special case of the API outlined above.
Here is what the implementation for post-content could look like: export function* getDependencies() {
return yield select( 'core/editor', 'getEditedPostAttribute', 'content' );
}
export function apply( attributeSchema, content ) {
return parse(content);
}
export function* update( attributeSchema, value ) {
yield editPost( { content: serialize(value) );
}Differences to custom sources used for attributes:
|
88286b8 to
062bcd0
Compare
|
I've pushed up some revisions:
As can be seen in the code, the revisions achieve the proposed interface mentioned in my previous comment. What still needs work:
|
|
Nice work! I think I have an idea that can kill both those remaining birds with one stone: export const { resetEditorBlocks, subscribeSources } = ( () => {
const lastDependencies = new WeakMap(); // Shared cache.
const updateDependencies = function*() {}; // Returns true if they have changed, and false if not.
return {
*resetEditorBlocks( blocks, options = {} ) {
if ( aCustomSourcedAttributedHasChanged ) {
callUpdates();
// This makes sure `subscribeSources` doesn't call back here unless something directly updates a source.
updateDependencies();
}
return {
type: 'RESET_EDITOR_BLOCKS',
// Reuses cache!
blocks: yield* getBlocksWithSourcedAttributes( blocks, lastDependencies ),
// Integrates with undo logic!
shouldCreateUndoLevel: options.__unstableShouldCreateUndoLevel !== false,
};
},
// Won't call `resetEditorBlocks` if `resetEditorBlocks` already updated dependencies.
*subscribeSources() {},
};
} )();This approach shares the cache, makes sure we only call |
|
@epiqueras Good idea. It seems to illustrate: Maybe we shouldn't bother with the IIFE at all, and just create a variable(s) in the shared top-level scope? This hints to another problem, however: Dependencies should be tracked unique per each registry. I have a few ideas for how we might incorporate this, but both add some further complexity to the solution:
If we hope for this sources behavior to become reusable at some point, these add a fair bit of overhead to how it would need to be implemented. At this point though, I think these goals could be optimized in future refactoring. |
|
Good catch!
I think we can implement your first suggestion easily by making
AWAIT_NEXT_STATE_CHANGE resolve to the registry, but your second suggestion
will scale to more use cases and fits our patterns better.
We can use the same part of the store we use for last attribute changes.
…On Mon, Jul 8, 2019 at 7:11 AM Andrew Duthie ***@***.***> wrote:
@epiqueras <https://github.com/epiqueras> Good idea. It seems to
illustrate: Maybe we shouldn't bother with the IIFE at all, and just create
a variable(s) in the shared top-level scope?
This hints to another problem, however: Dependencies should be tracked
unique per each registry. I have a few ideas for how we might incorporate
this, but both add some further complexity to the solution:
1. Change our dependency tracking to account *per registry*. For
example, lastDependencies.get( source ) becomes lastDependencies.get(
registry ).get( source ).
- We would need some way to get a reference to the registry from
within the action, likely via another control GET_REGISTRY:
createRegistryControl( ( registry ) => () => registry )
2. Manage last dependencies in state, via a reducer + selector
combination
If we hope for this sources behavior to become reusable at some point,
these add a fair bit of overhead to how it would need to be implemented. At
this point though, I think these goals could be optimized in future
refactoring.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#16402?email_source=notifications&email_token=AESFA2AZRSK7MPR5KWCZ3FLP6MVFPA5CNFSM4H47X6S2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODZM4KHQ#issuecomment-509199646>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AESFA2A4OJ3JMQCLRT4FR5DP6MVFPANCNFSM4H47X6SQ>
.
|
| blocks = synchronizeBlocksWithTemplate( blocks, template ); | ||
| } | ||
|
|
||
| yield resetEditorBlocks( blocks ); |
There was a problem hiding this comment.
I might run into it again in the future; I don't recall why I needed to reorder resetEditorBlocks, but it causes an issue where a new post will prompt about unsaved changes when SCRIPT_DEBUG is true, due to a fragile guarantee the current order establishes that causes "dirtiness" state to be unset by the setupEditorState action. This incidentally resolves the fact we dispatch this setupEditor action twice in an editing session, since it happens in constructor (rather than componentDidMount) and React helpfully detects side-effects via double-invoking intended non-side-effect lifecycle functions.
There was a problem hiding this comment.
Oh, I remember now: We want the sourced attributes to be applied in resetEditorBlocks, which requires that we set the post state first (so that post meta can be read).
In that case, we might need to address the underlying issue with the EditorProvider lifecycle, or endure the prompts in SCRIPT_DEBUG (preferably not).
There was a problem hiding this comment.
From our conversation:
So
resetEditorBlockssets the flag to true if the blocks are different.
setupEditorStatesets it to false again.
Basically it only worked because
setupEditorStatewas called as last, so it reset the dirty flag
The dirtying was actually pretty simple if we leave things as they are, and just call
resetPostbeforeresetEditorBlocks(so that meta values are available for applying)
There was a problem hiding this comment.
At some point, we should also look to refactor this so that the editor module doesn't need to store a copy of the "current" post, but instead calls to the @wordpress/core-data module to apply the edits on the canonical post object.
wp.data.select( 'core' ).getEntityRecord( 'postType', 'post', wp.data.select( 'core/editor' ).getCurrentPostId() );|
export function* getDependencies( schema ) {
return yield select( 'core/editor', 'getEditedPostAttribute', schema.postAttribute );
}For a |
|
Even better: export function* getDependencies( { attribute, property } ) {
const dependency = yield select( 'core/editor', 'getEditedPostAttribute', attribute );
return property ? _.get( dependency, property ) : dependency;
}For a title source: "attributes": {
"title": {
"type": "string",
"source": "post",
"attribute": "title"
}
}For meta: "attributes": {
"something": {
"type": "string",
"source": "post",
"attribute": "meta",
"property": "something"
}
} |
The problem with this is that it can be distinct by block type, rather than strictly for all blocks / block types of a given source. The specific meta property from which attribute values are sourced can vary by block type. |
A block may have multiple attributes which independently source uniquely from the same source type (e.g. two different meta properties)
Optimization since the attributes are likely a subset (most likely a single attribute), so we can avoid iterating all attributes defined in a block type.
d955e89 to
468f908
Compare
|
I've rebased to resolve conflicts introduced by #16184. I started squashing a few commits to clean up the history, but it got a bit unwieldy, so I left it more-or-less as it was (shouldn't matter much anyways since I'll Squash and Merge). I added a new commit 468f908 which removes an earlier approach to retrieving the registry in the resolution of I'm planning to merge this shortly. As follow-up tasks considered in this pull request:
|
|
Thanks @epiqueras and @youknowriad for your detailed feedback and suggestions here! |
mcsf
left a comment
There was a problem hiding this comment.
This is really great work that I've only now caught up with. Left some minor notes.
|
|
||
| if ( ! lastBlockSourceDependenciesByRegistry.has( registry ) ) { | ||
| continue; | ||
| } |
There was a problem hiding this comment.
Is the if condition expected to evaluate differently across for iterations? If not, we could place it as a condition for stepping into the for loop.
| lastBlockSourceDependenciesByRegistry.set( registry, new WeakMap ); | ||
| } | ||
|
|
||
| const lastBlockSourceDependencies = lastBlockSourceDependenciesByRegistry.get( registry ); |
There was a problem hiding this comment.
Can probably factor out this assignment and the optional WeakMap initialisation that comes before it.
Closes #16282
Related (alternative to): #16075
This pull request seeks to explore an implementation of custom sources.
Implementation Notes:
The implementation inherits a fair bit from #16075 (notably "last changes" tracking), but it tries to leverage a flow where sourced attributes are updated (as applicable) and applied prior to any blocks reset from the editor module. Furthermore, per discussion at #16075 (comment), they are implemented as standard store controls, where the benefit of this approach is in increased flexibility of the implementation by avoiding the need to determine a specific set of arguments which would apply for all potential custom source implementations. Instead, each implementation can simply yield controls (such as
selectfrom@wordpress/data-controls) to retrieve whichever data they depend upon.It diverges from some alternative proposals (#16282 (comment)) due to:
getBlockAttributesreturns a static reference)As to the proposed "Block Sources API", I'm not very attached to the specific set of arguments currently passed to
apply,applyAll, andupdate. The idea forapplyAllcame about as a result of a performance concern for repeated data access, but it is not strictly necessary (applyandupdatealone would be nicely complementing, though similarly we could explore to add anupdateAll). I didn't seek to make this publicly-extensible for the moment, though it is designed to allow for it in the future after some internal validation of the API.Remaining Tasks:
Testing Instructions:
Verify that updating a block with an attribute sourced by a meta attribute reflects the update (it is restored after a save, and it applies to all other blocks which source from the same meta).
As an example, I think this demo plugin should still work: https://gist.github.com/pento/19b35d621709042fc899e394a9387a54
Caveats:
parsebehavior and runs only after the meta values are applied.