Storybook: Alterar botões por meio de ações [ideia]

Criado em 9 jul. 2018  ·  55Comentários  ·  Fonte: storybookjs/storybook

Acabei de começar a usar o livro de histórias e até agora adoro. Uma coisa com a qual tenho lutado é como lidar com componentes puros "sem estado" que exigem que o estado esteja contido em um pai. Por exemplo, tenho uma caixa de seleção que aceita checked prop. Clicar na caixa de seleção não alterna seu estado, mas, em vez disso, dispara um onChange e espera para receber de volta um checked prop atualizado. Não parece haver nenhuma documentação sobre as melhores práticas para lidar com esses tipos de componentes, e as sugestões em questões como https://github.com/storybooks/storybook/issues/197 foram para criar um componente wrapper ou adicione outro complemento. Prefiro não fazer um invólucro de componente se puder evitar, porque gostaria de manter minhas histórias o mais simples possível.

Uma ideia que tive de lidar com isso é conectar o complemento actions com knobs , permitindo que os botões sejam alternados programaticamente por meio de ações. Como eu disse, sou novato em livros de histórias, então não tenho ideia se isso é viável, mas queria pelo menos levantar a sugestão.

Este é um tropeço para mim na implementação de histórias e imagino que possa ser para outras pessoas também. Se criar um componente wrapper realmente for a melhor coisa a fazer, talvez alguma documentação possa ser adicionada para esclarecer isso e como fazer isso?

knobs feature request todo

Comentários muito úteis

Estamos quase terminando o 5.3 (lançamento no início de janeiro) e procurando uma reescrita dos botões para 6.0 (lançamento no final de março) que resolverá um monte de problemas antigos dos botões. Vou ver se podemos resolver isso! Obrigado pela sua paciência!

Todos 55 comentários

Algum tipo de ideia semelhante foi introduzida neste PR # 3701, que foi controversa (e não foi fundida).

Podemos abrir uma discussão sobre isso novamente e ouvir as sugestões da API =).

Obrigado, eu não tinha visto aquele PR. Obrigado @aherriot por colocar isso juntos, parece que temos os mesmos pensamentos.

Antes de mergulhar em uma API, parece que o conceito fundamental precisa ser discutido e acordado. Um dos comentários no PR foi de @Hypnosphi :

Eu não gosto do fato de que ele introduz várias fontes de verdade (callbacks de componentes e botões de IU)

Parece-me que a ideia não é introduzir diferentes fontes de verdade, mas permitir manter uma fonte _única_ de verdade para o estado do componente - os botões. Todas as outras abordagens que vi sugeridas (componentes de invólucro, complementos de estado, recompor) introduziriam de fato outra fonte de verdade. Em meu exemplo de caixa de seleção, eu não seria capaz de ter um botão para checked e também permitir que um componente de invólucro fornecesse o checked prop. Eu vejo o painel de controle dos botões como o pai do componente, exceto que atualmente ele não pode obter nenhum retorno de chamada do componente, o que é meio unilateral e não como os aplicativos de reação são normalmente construídos.

Ao permitir que os botões sejam controlados programaticamente, a história pode ser isolada apenas para o componente sendo demonstrado, deixando o mecanismo de gerenciamento de estado real opaco, como deveria ser para um componente de apresentação simples como uma caixa de seleção. A caixa de seleção em si não importa como obtém seus adereços, pode ser conectada ao redux, seu pai pode usar setState , talvez use withState da recomposição, ou talvez os adereços sejam controlados por botões de livro de histórias.

De qualquer forma, como eu disse, sou muito novo nisso, então estou apenas compartilhando meus pensamentos intuitivos. Se eu estiver errado e houver uma "prática recomendada" geralmente aceita para lidar com esses tipos de componentes sem estado puros, talvez alguém possa me indicar um bom exemplo que eu possa seguir?

Oi :)

Só quero acrescentar que descobri que, à medida que meus componentes estão recebendo se devem exibir seus layouts móveis (ou não) a partir de adereços, meu objetivo era amarrar a mudança da janela de visualização ao meu botão, e teria sido muito bom;)

Só queria adicionar meu caso de uso aqui e, como @IanVS, até mesmo um retorno de chamada teria sido bom (se eu alternar meus adereços isMobile, poderia acionar uma mudança na janela de visualização)

Eu ouvi várias pessoas expressarem interesse em um método para atualizar o estado do botão. Se chegarmos a um acordo sobre uma boa arquitetura, acho que isso terá valor para muitas pessoas. Especialmente porque esta é uma funcionalidade extra que as pessoas podem escolher usar ou não e porque não afeta negativamente aqueles que não querem usá-la.

@Hypnosphi , você teve objeções sobre a implementação anterior, WDYT?

Comentando aqui para manter este problema aberto. Ainda estou curioso para saber o que

Parece-me que a ideia não é introduzir diferentes fontes de verdade, mas sim permitir manter uma única fonte de verdade para o estado do componente - os botões. Todas as outras abordagens que vi sugeridas (componentes de invólucro, complementos de estado, recompor) introduziriam de fato outra fonte de verdade. No meu exemplo de caixa de seleção, eu não seria capaz de ter um botão para marcado e também permitir que um componente de invólucro fornecesse o suporte marcado. Eu vejo o painel de controle dos botões como o pai do componente, exceto que atualmente ele não pode obter nenhum retorno de chamada do componente, o que é meio unilateral e não como os aplicativos de reação são normalmente construídos.

Parece razoável para mim. Vamos discutir a API

Este será um ótimo recurso. Acabei de enfrentar essa mesma necessidade aqui com um componente controlado muito básico e já tinha enfrentado isso antes em outros componentes também. Obrigado por olhar para ele.

Acompanhar a sintaxe atual parece um pequeno desafio. Talvez você pudesse usar algo como:

const {value: name, change: setName} = text('Name', 'Kent');

@brunoreis , eu me preocuparia que alterar a assinatura de retorno dos botões

import {boolean, changeBoolean} from '@storybook/addon-knobs/react';

stories.add('custom checkbox', () => (
    <MyCheckbox
        checked={boolean('checked', false)}
        onChange={(isChecked) => changeBoolean('checked', isChecked)} />
));

Onde o retorno de chamada onChange pode até permitir currying, para que isso também funcione:

onChange={(isChecked) => changeBoolean('checked')(isChecked)}

// which of course simplifies down to
onChange={changeBoolean('checked')}

A parte importante é que o primeiro argumento deve ser igual ao rótulo do botão que deve ser alterado. Acredito que isso permitiria aos usuários optar por esse comportamento sem alterar nada sobre a forma como os botões são usados ​​atualmente. (A menos que os rótulos não precisem ser exclusivos atualmente? Presumo que sejam ...)

@IanVS , isso é bom. Eu concordo com você sobre não mudar a maneira como as coisas funcionam. Não consegui encontrar uma maneira de fazer isso porque não pensei em usar o rótulo como uma chave. Isso pode funcionar. Vamos ver o que @Hypnosphi tem em mente.

alterar a assinatura de retorno dos botões seria uma alteração significativa

Tecnicamente, isso não é um problema agora. Temos um grande lançamento em breve. Mas, de fato, seria melhor permitir alguma compatibilidade com versões anteriores.

Eu gosto da ideia de currying support.

A menos que talvez os rótulos não precisem ser exclusivos atualmente?

Sim, eles são e, na verdade, são únicos em diferentes tipos. Portanto, não deve haver necessidade de change<Type> exportações separadas, apenas change deve ser o suficiente. Isso é basicamente o que foi feito em https://github.com/storybooks/storybook/pull/3701
Acho que vou reabrir esse PR para deixar @aherriot terminar com alguma ajuda de sua parte

Podemos ter isso:

const {value: name, change: setName} = text('Name', 'Kent');

sem ter que alterar o tipo de retorno de text .

As funções Javascript são objetos e, portanto, podem ter propriedades.
O objeto pode ser desestruturado.

screen shot 2018-09-19 at 00 08 35

@ndelangen a função text é de fato um objeto, mas seu valor de retorno não é. Isso não funcionará em seu exemplo:

const { foo, bar } = x() // note the parens

Poderíamos ter algo assim (o nome é questionável):

const {value: name, change: setName} = text.mutable('Name', 'Kent');

por que não vai funcionar?

const { foo, bar } = x() // note the parens

O retorno de x é uma função, que também pode ter propriedades.

screen shot 2018-09-23 at 14 12 28

Eu estava falando sobre x = () => {} do seu exemplo original.

Se fizermos text retornar uma função, os usuários precisarão alterar seu código:

// Before
<Foo bar={text('Bar', '')}>

// After
<Foo bar={text('Bar', '')()}>
                         ^^ this

eu vejo

Que tal a sugestão de

seria legal ter esse recurso.

que tal "destruir", como com "ganchos de reação"?:

const [foo, setFoo] = useString('foo', 'default');

Veio a dizer o mesmo que @DimaRGB
Seria capaz de preservar a sintaxe existente e adicionar chamadas em forma de gancho, por exemplo:

.add('example', () => {
  const [selected, setSelected] = useBool(false);

  return <SomeComponent selected={selected} onSelected={setSelected} />
})

A necessidade ocasional é que os componentes de reação não devem atualizar seus próprios props , mas, em vez disso, dizer aos pais que eles precisam ser alterados. Às vezes, um componente contém algum elemento que deve alternar seus adereços (me deparei com isso porque às vezes um menu da barra de navegação que tenho precisa se abrir se um elemento filho for clicado).

@IanVS Tenho exatamente a mesma situação em que tenho um componente de alternância que só é atualizado por meio de acessórios pais e, uma vez que o usuário de uma história de livro de histórias o alterna, o estado da IU é inconsistente com o que é mostrado no painel de botões. Eu o alterei usando um método privado de @storybook/addons-knobs - uma sugestão mais simples seria fornecer uma API oficial para fazer algo como:

import { manager } from '@storybook/addons-knob/dist/registerKnobs.js'

const { knobStore } = manager
// The name given to your knob - i.e:  `select("Checked", options, defaultValue)`
knobStore.store['Checked'].value = newValue
// Danger zone! _mayCallChannel() triggers a re-render of the _whole_ knobs form.
manager._mayCallChannel()

@erickwilder Eu tentei sua abordagem, mas isso nunca pareceu atualizar a forma do botão (mesmo quando atualizou os adereços fornecidos para o componente).

EDITAR:

risca isso; Só não vi essas atualizações porque estava usando options () com caixas de seleção que aparentemente funcionam de uma maneira 'não controlada'. Ao mudar para seleção múltipla e usar este método em um retorno de chamada, obtive os valores atualizados na forma de botão:

// Ditch when https://github.com/storybooks/storybook/issues/3855 gets resolved properly.
function FixMeKnobUpdate(name, value) {
    addons.getChannel().emit(CHANGE, { name, value });
}

Posso ter omitido alguns detalhes da minha configuração:

  • Estamos usando Vue.js com Storybook
  • O código que altera o valor e dispara a renderização novamente foi feito dentro de um método do componente de encapsulamento da história.

Não sei se essa abordagem funcionaria para todos.

Eu concordo totalmente com @IanVS , eu amo livros de histórias, mas realmente sinto falta da capacidade de atualizar botões por meio de ações. No meu caso, tenho dois componentes individuais A e B, mas em uma de minhas histórias quero mostrar uma composição usando os dois, de modo que sempre que mudar AI emita uma ação que modifique B e vice-versa.

Eu tentei o hack do @erickwilder com minha configuração (Angular 7), mas sempre que tento atualizar o armazenamento de botões, recebo o seguinte erro:

Erro: Esperava-se que não estivesse na Zona Angular, mas está!

Tentei de alguma forma executar isso fora do Angular, mas não consegui ... Na pior das hipóteses, vou criar um terceiro componente C que será um wrapper de A e B.

A solução temporária @erickwilder deve funcionar para qualquer framework, uma vez que não está relacionada ao framework / biblioteca (react, angular, vue, qualquer outra coisa), mas ao re-renderização do addon Knobs.

Funcionou para nós usando o React da mesma forma mencionada acima

+1 para adicionar um método publicamente visível para acionar, contendo o nome do botão como uma string a ser atualizada e um novo valor, por exemplo:

import { manager } from "@storybook/addon-knobs"

manager.updateKnob(
  propName, // knobs property, example from above "Checked"
  newValue, // new value to set programmatically, ex. true
)

Eu realmente gostaria que isso fosse oficialmente apoiado.

Nesse ínterim, com o Storybook v5, tive que usar esta solução terrivelmente hacky:

window.__STORYBOOK_ADDONS.channel.emit('storybookjs/knobs/change', {
  name: 'The name of my knob',
  value: 'the_new_value'
})

🙈

🙈

Relacionado: # 6916

Isso seria legal ... Principalmente porque modais.

Eu acho que isso seria útil.

Exemplo:

storiesOf('input', module)
  .addDecorator(withKnobs)
  .add('default', () => <input type={text('type', 'text')} value={text('value', '')} disabled={boolean('disabled', false)} placeholder={text('placeholder', '')} onChange={action('onChange')} />)

Isso atualmente funciona, mas parece natural que queiramos conectar o valor de destino do evento onChange ao valor definido em props do elemento testado. Isso demonstraria melhor a aplicação do elemento.

Isso seria útil

+1 para @mathieuk / @raspo por apontar para uma solução aqui.

Também podemos ter acesso a isso de outra maneira. Criando alguns métodos genéricos em um módulo helpers ?

import addons from "@storybook/addons";

export const emitter = (type, options) => addons.getChannel().emit(type, options);

export const updateKnob = (name, value) => (
  emitter("storybookjs/knobs/change", { name, value })
);

E ligando conforme necessário nas histórias ...

import { text } from "@storybook/addon-knobs";
import { updateKnob } from 'helpers';

// ...
const value = text("value", "Initial value");
<select
  value={value}
  onChange={({ target }) => updateKnob("value", target.value)}
>

... mas ainda parece uma solução alternativa de ligação bidirecional para algo que poderia ser integrado à API de botões

Alguma atualização sobre o acima? Seria um recurso muito útil 👍

Esse recurso sendo implementado nos permitiria fazer alguns botões de dependência cruzada realmente poderosos com bastante facilidade. Imagine fazer uma história de componente de paginação se você pudesse ter um botão atualizado dinamicamente dependendo de outro, com algo assim em sua história:

const resultsCount = number(
    'Results count',
    currentPage, {
        max: 100,
        min: 0,
        range: true
    }
);
const resultsArray: React.ReactNode[] = new Array(resultsCount)
    .fill(true)
    .map((_, idx) => <div key={idx + 1}>Test Pagination Result #{idx + 1}</div>);
const childrenPerPage = 10;
const currentPage = number(
    'Current page index',
    0, {
        max: Math.ceil(resultsCount / childrenPerPage) - 1,
        min: 0,
        range: true
    }
);

Eu tentei fazer essencialmente isso, esperando que o intervalo máximo em meu botão currentPage fosse atualizado dinamicamente ao aumentar o botão resultsCount em um incremento de 10, no entanto, parece o valor inicial de cada botão é armazenado em cache no momento da criação e não é usado para substituir valores de estado interno em renderizações subsequentes. Com o código acima, quando eu aumentar resultsCount em 10+, eu esperaria que max de currentPage também aumentasse em 1, no entanto, seu valor permanece o mesmo, apesar dos valores subjacentes serem usado para calculá-lo tendo mudado (o registro Math.ceil(resultsCount / childrenPerPage) - 1 mostra o novo valor esperado).

Estamos quase terminando o 5.3 (lançamento no início de janeiro) e procurando uma reescrita dos botões para 6.0 (lançamento no final de março) que resolverá um monte de problemas antigos dos botões. Vou ver se podemos resolver isso! Obrigado pela sua paciência!

@shilman , estou muito animado com esse recurso 😄

Eu, @atanasster e @ PlayMa256 estamos trabalhando na base para isso há algum tempo. Serão necessárias mais algumas iterações para acertar, mas estou muito confiante de que podemos chegar a 100% na versão 6.0.0 e realmente revolucionar os dados e a reatividade do livro de histórias.

Revolucionar? Quente maldita diggity. Pare de me provocar, meu corpo está pronto.

1 para modais :)

Não posso acreditar que é impossível desde o início. Esperando por atualizações ...

Oi pessoal!
Como solução temporária, usei no próximo código do meu aplicativo:

import { addons } from '@storybook/addons';
import { CHANGE } from '@storybook/addon-knobs';

const channel = addons.getChannel();

channel.emit(CHANGE, {
  name: 'prop_name',
  value: prop_value,
});

Ainda esperando esse recurso será implementado de forma nativa.

Resolvi esse problema usando observáveis. Estou usando um livro de histórias com angular

`
.add ('atualizando dados do gráfico', () => {

const myObservable= new BehaviorSubject([{a: 'a', b: 'b'}]);
return {
  template: '
  <my-component
    myInput: myData,
    (myEvent)="myEventProp($event)"
  ></my-component>
  ',
  props: {
    myData: myObservable,
    myEventProp: $event => {
      myObservable.next([]);
      action('(myEvent)')($event);
    }
  }
};

})
`

@ norbert-doofus obrigado seu exemplo me ajudou 👍

Olá, pessoal. Acabamos de lançar os controles adicionais na versão 6.0-beta!

Os controles são botões portáteis, gerados automaticamente, que se destinam a substituir os botões adicionais a longo prazo.

Atualize e experimente-os hoje. Obrigado por sua ajuda e suporte para obter este estável para lançamento!

Isso é ótimo! Não consigo dizer ao ler o README (posso ter perdido), esses novos controls ser alterados programaticamente, qual foi a solicitação feita nesta edição?

Eles podem ser, embora eu não tenha certeza de que a API seja oficialmente suportada ainda (embora estejamos fazendo exatamente isso para os controles em addon-docs). Trabalharei com @tmeasday para descobrir a melhor maneira de conseguir isso após a primeira rodada de bugs de controle se estabilizarem.

  • [] adicionar updateArgs ao contexto da história?
  • [] tornar o contexto da história disponível para um retorno de chamada que é chamado a partir da história? ( this.context?.updateArgs(....) )

Para qualquer pessoa interessada em controles, mas não sabe por onde começar, criei um passo a passo rápido e sujo para ir de um novo projeto CRA a uma demonstração funcional. Confira:

=> Controles de Storybook com CRA e TypeScript

Existem também alguns documentos de migração de "botões para controles" no README de controles:

=> Como faço para migrar de botões de expansão?

Oi pessoal!
Como solução temporária, usei no próximo código do meu aplicativo:

import { addons } from '@storybook/addons';
import { CHANGE } from '@storybook/addon-knobs';

const channel = addons.getChannel();

channel.emit(CHANGE, {
  name: 'prop_name',
  value: prop_value,
});

Ainda esperando esse recurso será implementado de forma nativa.

Lembre-se de que, ao usar groupId você precisará adicionar o ID do grupo a name seguinte maneira:

 const show = boolean('Show Something', true, 'Group')

channel.emit(CHANGE, {
  name: 'Show Something_Group',
  value: prop_value,
});

Além disso, channel é um EventEmitter, então você pode addListener nele para verificar quais são os parâmetros sendo recebidos.

channel.addListener(CHANGE, console.log)

Aqui está um trecho de código para qualquer pessoa interessada em como fazer isso usando addon-controls na v6.

import { useArgs } from '@storybook/client-api';

// Inside a story
export const Basic = ({ label, counter }) => {
    const [args, updateArgs] = useArgs();
    return <Button onClick={() => updateArgs({ counter: counter+1 })>{label}: {counter}<Button>;
}

Não sei se esta é a melhor API para esse propósito, mas deve funcionar. Exemplo no monorepo:

https://github.com/storybookjs/storybook/blob/next/examples/official-storybook/stories/core/args.stories.js#L34 -L43

Obrigado, @shilman! Isso funcionou.

Para as pessoas interessadas, acontece que temos a Checkbox story checked exata que iniciou todo este tópico. Aqui está nossa história recém-conectada usando o Storybook 6.0.0-rc.9:

export const checkbox = (args) => {
  const [{ checked }, updateArgs] = useArgs();
  const toggleChecked = () => updateArgs({ checked: !checked });
  return <Checkbox {...args} onChange={toggleChecked} />;
};
checkbox.args = {
  checked: false,
  label: 'hello checkbox!',
};
checkbox.argTypes = {
  checked: { control: 'boolean' },
};

cb-arg

@shilman tentei utilizar useArgs em uma história para uma entrada de texto controlada (um caso em que normalmente podemos usar o gancho useState para atualizar o value prop do componente por meio de seu evento onChange ). No entanto, encontrei um problema em que sempre que o usuário digita um caractere, o componente perde o foco. Isso talvez seja causado por ele atualizar / renderizar novamente a história toda vez que atualizamos os argumentos?

Existe um método diferente recomendado para utilizar argumentos / controles para um componente com entrada de texto controlada?

Isso foi com 6.0.0-rc.13

@jcq Você pode criar um novo problema com um repro? Este não foi o caso de uso principal para useArgs , mas certamente um que gostaríamos de oferecer suporte, portanto, ficaríamos felizes em investigá-lo.

@shilman Sem problemas - aqui está o novo problema:
https://github.com/storybookjs/storybook/issues/11657

Eu também deveria ter esclarecido que o comportamento errôneo aparece no Docs, embora funcione corretamente no modo Canvas normal.

Resolvi esse problema usando observáveis. Estou usando um livro de histórias com angular

`
.add ('atualizando dados do gráfico', () => {

const myObservable= new BehaviorSubject([{a: 'a', b: 'b'}]);
return {
  template: '
  <my-component
    myInput: myData,
    (myEvent)="myEventProp($event)"
  ></my-component>
  ',
  props: {
    myData: myObservable,
    myEventProp: $event => {
      myObservable.next([]);
      action('(myEvent)')($event);
    }
  }
};

})
`

Isso funcionou para mim usando Angular, mas mudando para myData.value na linha 5

Ainda não experimentei (estou preso em uma versão mais antiga do livro de histórias por enquanto), mas parece que esse problema pode ser resolvido agora. Obrigado pelo excelente trabalho em args / controles!

Esta página foi útil?
0 / 5 - 0 avaliações